diff --git a/packages/nc-gui/components/cell/Currency/Editor.vue b/packages/nc-gui/components/cell/Currency/Editor.vue index 35ac18df4783..656d7ec49c75 100644 --- a/packages/nc-gui/components/cell/Currency/Editor.vue +++ b/packages/nc-gui/components/cell/Currency/Editor.vue @@ -23,7 +23,7 @@ const _vModel = useVModel(props, 'modelValue', emit) const lastSaved = ref() const cellFocused = ref(false) -const inputType = computed(() => (!isForm.value && !cellFocused.value ? 'text' : 'number')) +const inputType = computed(() => (isExpandedFormOpen.value && !cellFocused.value ? 'text' : 'number')) const currencyMeta = computed(() => { return { diff --git a/packages/nc-gui/components/cell/Currency/index.vue b/packages/nc-gui/components/cell/Currency/index.vue index da564b44ac46..9156e073ca01 100644 --- a/packages/nc-gui/components/cell/Currency/index.vue +++ b/packages/nc-gui/components/cell/Currency/index.vue @@ -29,7 +29,7 @@ const _vModel = useVModel(props, 'modelValue', emit) const cellFocused = ref(false) -const inputType = computed(() => (!isForm.value && !cellFocused.value ? 'text' : 'number')) +const inputType = computed(() => (isExpandedFormOpen.value && !cellFocused.value ? 'text' : 'number')) const vModel = computed({ get: () => _vModel.value, diff --git a/packages/nc-gui/components/cell/Decimal/Editor.vue b/packages/nc-gui/components/cell/Decimal/Editor.vue index a99827528955..ab0501c8e7c4 100644 --- a/packages/nc-gui/components/cell/Decimal/Editor.vue +++ b/packages/nc-gui/components/cell/Decimal/Editor.vue @@ -21,6 +21,7 @@ const readOnly = inject(ReadonlyInj, ref(false)) const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isForm = inject(IsFormInj)! const isCanvasInjected = inject(IsCanvasInjectionInj, false) +const canvasCellEventData = inject(CanvasCellEventDataInj, reactive({})) const inputRef = ref() const vModel = useVModel(props, 'modelValue', emits) @@ -30,6 +31,9 @@ const precision = computed(() => { }) onMounted(() => { + if (canvasCellEventData?.keyboardKey && isSinglePrintableKey(canvasCellEventData?.keyboardKey)) { + vModel.value = Number(canvasCellEventData.keyboardKey) + } if (isCanvasInjected && !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value) { inputRef.value?.focus() } diff --git a/packages/nc-gui/components/cell/Time/index.vue b/packages/nc-gui/components/cell/Time/index.vue index 0e38a3dab540..6d3fbe6ab35c 100644 --- a/packages/nc-gui/components/cell/Time/index.vue +++ b/packages/nc-gui/components/cell/Time/index.vue @@ -55,7 +55,7 @@ const localState = computed({ return undefined } let convertingValue = modelValue - const valueNumber: number = Number(modelValue) + const valueNumber = Number(modelValue) if (!isNaN(valueNumber)) { // FIXME: currently returned value is in minutes // so need to * 60 and need to be removed if changed to seconds diff --git a/packages/nc-gui/components/dlg/AirtableImport.vue b/packages/nc-gui/components/dlg/AirtableImport.vue index 8c734e864af8..ce3fbc9f2a07 100644 --- a/packages/nc-gui/components/dlg/AirtableImport.vue +++ b/packages/nc-gui/components/dlg/AirtableImport.vue @@ -6,6 +6,7 @@ const { modelValue, baseId, sourceId, transition } = defineProps<{ baseId: string sourceId: string transition?: string + showBackBtn?: boolean }>() const emit = defineEmits(['update:modelValue', 'back']) @@ -450,7 +451,9 @@ const collapseKey = ref('') } " > - {{ $t('general.back') }} + + + {{ showBackBtn ? $t('general.back') : $t('general.cancel') }} { const jobData = await api.base.duplicate(props.base.id as string, { options: optionsToExclude.value, base: { - fk_workspace_id: isEeUI - ? targetWorkspace.value?.id - ? targetWorkspace.value.id - : props.base.fk_workspace_id - : null, + fk_workspace_id: isEeUI ? (targetWorkspace.value?.id ? targetWorkspace.value.id : props.base.fk_workspace_id) : null, type: props.base.type, color, meta: JSON.stringify({ diff --git a/packages/nc-gui/components/dlg/NocoDbImport.vue b/packages/nc-gui/components/dlg/NocoDbImport.vue index 91fe69e6dce7..5dbafd38648c 100644 --- a/packages/nc-gui/components/dlg/NocoDbImport.vue +++ b/packages/nc-gui/components/dlg/NocoDbImport.vue @@ -5,6 +5,7 @@ const { modelValue, baseId, transition } = defineProps<{ modelValue: boolean baseId: string transition?: string + showBackBtn?: boolean }>() const emit = defineEmits(['update:modelValue', 'back']) @@ -347,7 +348,9 @@ onUnmounted(() => { } " > - {{ $t('general.back') }} + + + {{ showBackBtn ? $t('general.back') : $t('general.cancel') }} {{ $t('general.abort') }} diff --git a/packages/nc-gui/components/dlg/QuickImport.vue b/packages/nc-gui/components/dlg/QuickImport.vue index 207f0cdd2e05..f2a10951e3ba 100644 --- a/packages/nc-gui/components/dlg/QuickImport.vue +++ b/packages/nc-gui/components/dlg/QuickImport.vue @@ -2,8 +2,9 @@ import { toRaw, unref } from '@vue/runtime-core' import type { UploadChangeParam, UploadFile } from 'ant-design-vue' import { Upload } from 'ant-design-vue' -import { type TableType, charsetOptions } from 'nocodb-sdk' +import { type TableType, charsetOptions, charsetOptionsMap, ncHasProperties } from 'nocodb-sdk' import rfdc from 'rfdc' +import type { ProgressMessageObjType } from '../../helpers/parsers/TemplateGenerator' interface Props { modelValue: boolean @@ -12,12 +13,19 @@ interface Props { sourceId: string importDataOnly?: boolean transition?: string + showBackBtn?: boolean } -const { importType, importDataOnly = false, baseId, sourceId, transition, ...rest } = defineProps() +const { importType, importDataOnly = false, baseId, sourceId, transition, showBackBtn, ...rest } = defineProps() const emit = defineEmits(['update:modelValue', 'back']) +enum ImportTypeTabs { + 'upload' = 'upload', + 'uploadFromUrl' = 'uploadFromUrl', + 'uploadJSON' = 'uploadJSON', +} + const { $api, $importWorker } = useNuxtApp() let importWorker: Worker @@ -35,6 +43,7 @@ const isWorkerSupport = typeof Worker !== 'undefined' const { t } = useI18n() const progressMsg = ref('Parsing Data ...') +const progressMsgNew = ref>({}) const { tables } = storeToRefs(useBase()) @@ -64,6 +73,12 @@ const temporaryJson = ref({}) const jsonErrorText = ref('') +const activeTab = ref(ImportTypeTabs.upload) + +const isError = ref(false) + +const refMonacoEditor = ref() + const useForm = Form.useForm const defaultImportState = { @@ -140,10 +155,6 @@ watch( { immediate: true }, ) -const filterOption = (input = '', params: { key: string }) => { - return params.key?.toLowerCase().includes(input.toLowerCase()) -} - const isPreImportFileFilled = computed(() => { return importState.fileList?.length > 0 }) @@ -153,21 +164,52 @@ const isPreImportUrlFilled = computed(() => { }) const isPreImportJsonFilled = computed(() => { - return JSON.stringify(importState.jsonEditor).length > 2 && !jsonErrorText.value + try { + return refMonacoEditor.value.isValid && JSON.stringify(importState.jsonEditor).length > 2 + } catch { + return false + } }) +const localImportError = ref('') + +const importError = computed(() => localImportError.value ?? templateEditorRef.value?.importError ?? '') + +const maxFileUploadLimit = computed(() => (isImportTypeCsv.value ? 3 : 1)) + +const hideUpload = computed(() => preImportLoading.value || importState.fileList.length >= maxFileUploadLimit.value) + const disablePreImportButton = computed(() => { - if (isImportTypeCsv.value) { - return isPreImportFileFilled.value === isPreImportUrlFilled.value - } else if (IsImportTypeExcel.value) { - return isPreImportFileFilled.value === isPreImportUrlFilled.value - } else if (isImportTypeJson.value) { - return !isPreImportFileFilled.value && !isPreImportJsonFilled.value + if (activeTab.value === ImportTypeTabs.upload) { + return !isPreImportFileFilled.value + } else if (activeTab.value === ImportTypeTabs.uploadFromUrl) { + return !isPreImportUrlFilled.value + } else if (activeTab.value === ImportTypeTabs.uploadJSON) { + return !isPreImportJsonFilled.value } + + return true }) -const isError = ref(false) -const refMonacoEditor = ref() +const importBtnText = computed(() => { + // configure field screen + if (templateEditorModal.value) { + if (importLoading.value) { + return importDataOnly ? t('labels.uploading') : t('labels.importing') + } + + return importDataOnly ? t('activity.upload') : t('activity.import') + } + + const type = isImportTypeJson.value ? t('labels.jsonCapitalized') : t('objects.files') + + // upload file screen + if (preImportLoading.value) { + return importDataOnly ? `${t('labels.uploading')} ${type}` : `${t('labels.importing')} ${type}` + } + + return importDataOnly ? `${t('activity.upload')} ${type}` : `${t('activity.import')} ${type}` +}) const disableImportButton = computed(() => !templateEditorRef.value?.isValid || isError.value) @@ -176,34 +218,37 @@ let templateGenerator: CSVTemplateAdapter | JSONTemplateAdapter | ExcelTemplateA async function handlePreImport() { preImportLoading.value = true isParsingData.value = true + localImportError.value = '' if (!baseTables.value.get(baseId)) { await loadProjectTables(baseId) } + const isPreImportFileMode = isPreImportFileFilled.value && activeTab.value === ImportTypeTabs.upload + if (isImportTypeCsv.value) { - if (isPreImportFileFilled.value) { + if (isPreImportFileMode) { await parseAndExtractData(importState.fileList as streamImportFileList) } else if (isPreImportUrlFilled.value) { try { await validate() await parseAndExtractData(importState.url) } catch (e: any) { - message.error(await extractSdkResponseErrorMsg(e)) + localImportError.value = await extractSdkResponseErrorMsg(e) } } } else if (isImportTypeJson.value) { - if (isPreImportFileFilled.value) { + if (isPreImportFileMode) { if (isWorkerSupport && importWorker) { await parseAndExtractData(importState.fileList as streamImportFileList) } else { await parseAndExtractData((importState.fileList as importFileList)[0].data) } - } else { + } else if (isPreImportJsonFilled.value) { await parseAndExtractData(JSON.stringify(importState.jsonEditor)) } } else if (IsImportTypeExcel) { - if (isPreImportFileFilled.value) { + if (isPreImportFileMode) { if (isWorkerSupport && importWorker) { await parseAndExtractData(importState.fileList as streamImportFileList) } else { @@ -214,7 +259,7 @@ async function handlePreImport() { await validate() await parseAndExtractData(importState.url) } catch (e: any) { - message.error(await extractSdkResponseErrorMsg(e)) + localImportError.value = await extractSdkResponseErrorMsg(e) } } } @@ -224,21 +269,27 @@ async function handlePreImport() { } async function handleImport() { + localImportError.value = '' try { if (!templateGenerator && !importWorker) { - message.error(t('msg.error.templateGeneratorNotFound')) + localImportError.value = t('msg.error.templateGeneratorNotFound') return } importLoading.value = true await templateEditorRef.value.importTemplate() + + templateEditorModal.value = false + Object.assign(importState, defaultImportState) + dialogShow.value = false } catch (e: any) { - return message.error(await extractSdkResponseErrorMsg(e)) + console.log(e) + + const errorMsg = await extractSdkResponseErrorMsg(e) + localImportError.value = errorMsg + return } finally { importLoading.value = false - templateEditorModal.value = false - Object.assign(importState, defaultImportState) } - dialogShow.value = false } function rejectDrop(fileList: UploadFile[]) { @@ -249,6 +300,7 @@ function rejectDrop(fileList: UploadFile[]) { function handleChange(info: UploadChangeParam) { const status = info.file.status + if (status && status !== 'uploading' && status !== 'removed') { if (isImportTypeCsv.value || (isWorkerSupport && importWorker)) { if (!importState.fileList.find((f) => f.uid === info.file.uid)) { @@ -284,9 +336,7 @@ function handleChange(info: UploadChangeParam) { } } - if (status === 'done') { - message.success(`Uploaded file ${info.file.name} successfully`) - } else if (status === 'error') { + if (status === 'error') { message.error(`${t('msg.error.fileUploadFailed')} ${info.file.name}`) } } @@ -312,8 +362,10 @@ function populateUniqueTableName(tn: string, draftTn: string[] = []) { } function getAdapter(val: any) { + const isPreImportFileMode = isPreImportFileFilled.value && activeTab.value === ImportTypeTabs.upload + if (isImportTypeCsv.value) { - if (isPreImportFileFilled.value) { + if (isPreImportFileMode) { return new CSVTemplateAdapter( val, { @@ -335,13 +387,13 @@ function getAdapter(val: any) { ) } } else if (IsImportTypeExcel.value) { - if (isPreImportFileFilled.value) { + if (isPreImportFileMode) { return new ExcelTemplateAdapter(val, importState.parserConfig, undefined, undefined, unref(existingColumns)) } else { return new ExcelUrlTemplateAdapter(val, importState.parserConfig, $api, undefined, undefined, unref(existingColumns)) } } else if (isImportTypeJson.value) { - if (isPreImportFileFilled.value) { + if (isPreImportFileMode) { return new JSONTemplateAdapter(val, importState.parserConfig) } else { return new JSONTemplateAdapter(val, importState.parserConfig) @@ -366,8 +418,14 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => { customReqArgs.onSuccess() } +const showMaxFileLimitError = ref(false) + /** check if the file size exceeds the limit */ -const beforeUpload = (file: UploadFile) => { +const beforeUpload = (file: UploadFile, fileList: UploadFile[]) => { + if (importState.fileList.length + fileList.length > maxFileUploadLimit.value) { + showMaxFileLimitError.value = true + } + const exceedLimit = file.size! / 1024 / 1024 > 25 if (exceedLimit) { message.error(`File ${file.name} is too big. The accepted file size is less than 25MB.`) @@ -389,7 +447,10 @@ function extractImportWorkerPayload(value: UploadFile[] | ArrayBuffer | string) importType = importType! ?? ImportType.CSV let importSource: ImportSource - if (isPreImportFileFilled.value) { + + const isPreImportFileMode = isPreImportFileFilled.value && activeTab.value === ImportTypeTabs.upload + + if (isPreImportFileMode) { importSource = ImportSource.FILE } else if (isPreImportUrlFilled.value && importType !== ImportType.JSON) { importSource = ImportSource.URL @@ -462,7 +523,12 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) { importWorker?.removeEventListener('message', handler, false) break case ImportWorkerResponse.PROGRESS: - progressMsg.value = payload + if (ncHasProperties(payload, ['title', 'value'])) { + progressMsgNew.value = { ...progressMsgNew.value, [payload.title]: payload?.value ?? '' } + } else { + progressMsg.value = payload + } + break case ImportWorkerResponse.ERROR: reject(payload) @@ -483,7 +549,7 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) { templateGenerator = getAdapter(val) if (!templateGenerator) { - message.error(t('msg.error.templateGeneratorNotFound')) + localImportError.value = t('msg.error.templateGeneratorNotFound') return } @@ -507,9 +573,19 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) { } templateEditorModal.value = true + showMaxFileLimitError.value = false } catch (e: any) { console.log(e) - message.error(await extractSdkResponseErrorMsg(e)) + + /** + * If it is import url and it fail to send req due to cross origin or any other reason the e type will be string + * @example: Failed to execute 'send' on 'XMLHttpRequest': Failed to load '' + */ + if (typeof e === 'string' && isPreImportUrlFilled.value && activeTab.value === ImportTypeTabs.uploadFromUrl) { + localImportError.value = e.replace(importState.url, '').replace(/''/, '') + } else { + localImportError.value = (await extractSdkResponseErrorMsg(e)) || e?.toString() + } } } @@ -526,10 +602,33 @@ onMounted(() => { importState.parserConfig.autoSelectFieldTypes = importDataOnly }) -onUnmounted(() => { +const onCancelImport = () => { $importWorker.terminate() + Object.assign(importState, defaultImportState) + preImportLoading.value = false + importLoading.value = false + templateData.value = undefined + importData.value = undefined + importColumns.value = [] + + templateEditorModal.value = false + isParsingData.value = false + temporaryJson.value = {} + jsonErrorText.value = '' + isError.value = false + localImportError.value = '' +} + +onUnmounted(() => { + onCancelImport() }) +const onClickCancel = () => { + dialogShow.value = false + emit('back') + onCancelImport() +} + function handleJsonChange(newValue: any) { try { temporaryJson.value = newValue @@ -540,6 +639,11 @@ function handleJsonChange(newValue: any) { } } +function handleResetImportError() { + localImportError.value = '' + templateEditorRef.value?.updateImportError?.('') +} + watch( () => importState.fileList, () => { @@ -556,6 +660,11 @@ watch( } }, 500) } + + // Hide max file limit error on removing file + if (importState.fileList.length < maxFileUploadLimit.value && showMaxFileLimitError.value) { + showMaxFileLimitError.value = false + } }, ) @@ -571,13 +680,18 @@ watch( :transition-name="transition" @keydown.esc="dialogShow = false" > - +
{{ importMeta.header }} @@ -585,7 +699,12 @@ watch(
-
+
- - - -

- {{ $t('msg.dropYourDocHere') }} {{ $t('general.or').toLowerCase() }} - {{ $t('labels.browseFiles') }} -

- -

{{ $t('general.supported') }}: {{ importMeta.acceptTypes }}

- -

- {{ importMeta.uploadHint }} -

- -