Browse Source

Merge pull request #6990 from nocodb/nc-fix/csv-import-bug-fix

Nc fix/csv import bug fix
pull/6994/head
Raju Udava 1 year ago committed by GitHub
parent
commit
661625b062
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      packages/nc-gui/components/dlg/QuickImport.vue
  2. 85
      packages/nc-gui/components/template/Editor.vue
  3. 10
      packages/nc-gui/composables/useColumnCreateStore.ts
  4. 22
      packages/nc-gui/utils/validation.ts
  5. 4
      packages/nocodb/src/services/tables.service.ts
  6. 6
      tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

14
packages/nc-gui/components/dlg/QuickImport.vue

@ -172,7 +172,9 @@ const disablePreImportButton = computed(() => {
}
})
const disableImportButton = computed(() => !templateEditorRef.value?.isValid)
const isError = ref(false)
const disableImportButton = computed(() => !templateEditorRef.value?.isValid || isError.value)
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid)
@ -530,6 +532,14 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
preImportLoading.value = false
}
}
const onError = () => {
isError.value = true
}
const onChange = () => {
isError.value = false
}
</script>
<template>
@ -558,6 +568,8 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
:import-worker="importWorker"
class="nc-quick-import-template-editor"
@import="handleImport"
@error="onError"
@change="onChange"
/>
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" tab-position="top">

85
packages/nc-gui/components/template/Editor.vue

@ -34,12 +34,13 @@ import {
useI18n,
useNuxtApp,
useTabs,
validateTableName,
} from '#imports'
const { quickImportType, baseTemplate, importData, importColumns, importDataOnly, maxRowsToParse, sourceId, importWorker } =
defineProps<Props>()
const emit = defineEmits(['import'])
const emit = defineEmits(['import', 'error', 'change'])
dayjs.extend(utc)
@ -95,6 +96,8 @@ const importingTips = ref<Record<string, string>>({})
const checkAllRecord = ref<boolean[]>([])
const formError = ref()
const uiTypeOptions = ref<Option[]>(
(Object.keys(UITypes) as (keyof typeof UITypes)[])
.filter(
@ -124,11 +127,14 @@ const data = reactive<{
const validators = computed(() =>
data.tables.reduce<Record<string, [ReturnType<typeof fieldRequiredValidator>]>>((acc: Record<string, any>, table, tableIdx) => {
acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator()]
acc[`tables.${tableIdx}.table_name`] = [validateTableName]
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator(), fieldLengthValidator()]
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true
@ -139,10 +145,12 @@ const validators = computed(() =>
}, {}),
)
const { validate, validateInfos } = useForm(data, validators)
const { validate, validateInfos, modelRef } = useForm(data, validators)
const isValid = ref(!importDataOnly)
const formRef = ref()
watch(
() => srcDestMapping.value,
() => {
@ -674,6 +682,39 @@ function handleUIDTChange(column, table) {
])
}
}
const setErrorState = (errorsFields: any[]) => {
const errorMap: any = {}
for (const error of errorsFields) {
errorMap[error.name] = error.errors
}
formError.value = errorMap
}
watch(formRef, () => {
setTimeout(async () => {
try {
await validate()
emit('change')
formError.value = null
} catch (e: any) {
emit('error', e)
setErrorState(e.errorFields)
}
}, 500)
})
watch(modelRef, async () => {
try {
await validate()
emit('change')
formError.value = null
} catch (e: any) {
emit('error', e)
setErrorState(e.errorFields)
}
})
</script>
<template>
@ -694,7 +735,7 @@ function handleUIDTChange(column, table) {
<a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion>
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<span class="font-weight-bold text-lg flex items-center gap-2">
<span class="font-weight-bold text-lg flex items-center gap-2 truncate">
<component :is="iconMap.table" class="text-primary" />
{{ table.table_name }}
</span>
@ -769,7 +810,7 @@ function handleUIDTChange(column, table) {
</a-card>
<a-card v-else>
<a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<a-form ref="formRef" :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<p v-if="data.tables && quickImportType === 'excel'" class="text-center">
{{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }}
available for import
@ -783,22 +824,24 @@ function handleUIDTChange(column, table) {
>
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<a-form-item v-if="editableTn[tableIdx]" v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<a-input
v-model:value.lazy="table.table_name"
class="max-w-xs font-weight-bold text-lg"
size="large"
hide-details
:bordered="false"
@click.stop
@blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)"
/>
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<div class="flex flex-col w-full">
<a-input
v-model:value="table.table_name"
class="font-weight-bold text-lg"
size="large"
hide-details
:bordered="false"
@click.stop
@blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)"
@dblclick="setEditableTn(tableIdx, true)"
/>
<div v-if="formError?.[`tables.${tableIdx}.table_name`]" class="text-red-500 ml-3">
{{ formError?.[`tables.${tableIdx}.table_name`].join('\n') }}
</div>
</div>
</a-form-item>
<span v-else class="font-weight-bold text-lg flex items-center gap-2" @click="setEditableTn(tableIdx, true)">
<component :is="iconMap.table" class="text-primary" />
{{ table.table_name }}
</span>
</template>
<template #extra>

10
packages/nc-gui/composables/useColumnCreateStore.ts

@ -40,6 +40,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { sqlUis } = storeToRefs(baseStore)
const { bases } = storeToRefs(useBases())
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
@ -64,6 +66,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]),
)
const source = computed(() =>
meta.value && meta.value.source_id && meta.value.base_id
? bases.value.get(meta.value?.base_id as string)?.sources?.find((s) => s.id === meta.value!.source_id)
: undefined,
)
const idType = null
const additionalValidations = ref<ValidationsObj>({})
@ -128,7 +136,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
})
},
},
fieldLengthValidator(),
fieldLengthValidator(source.value?.type || ClientType.MYSQL),
],
uidt: [
{

22
packages/nc-gui/utils/validation.ts

@ -13,6 +13,10 @@ export const validateTableName = {
return reject(new Error(t('msg.error.tableNameRequired')))
}
if (value.length > 52) {
return reject(new Error(t('msg.error.columnNameExceedsCharacters', { value: 52 })))
}
// exclude . / \
// rest all characters allowed
// https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/acreldb/n0rfg6x1shw0ppn1cwhco6yn09f7.htm#:~:text=By%20default%2C%20MySQL%20encloses%20column,not%20truncate%20a%20longer%20name.
@ -98,17 +102,21 @@ export const fieldRequiredValidator = () => {
}
}
export const fieldLengthValidator = () => {
export const fieldLengthValidator = (sqlClientType: string) => {
return {
validator: (rule: any, value: any) => {
const { t } = getI18n().global
// mysql allows 64 characters for column_name
// postgres allows 59 characters for column_name
// mssql allows 128 characters for column_name
// sqlite allows any number of characters for column_name
// We allow 255 for all databases, truncate will be handled by backend for column_name
const fieldLengthLimit = 255
// no limit for sqlite but set as 255
let fieldLengthLimit = 255
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
fieldLengthLimit = 64
} else if (sqlClientType === 'pg') {
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
return new Promise((resolve, reject) => {
if (value?.length > fieldLengthLimit) {

4
packages/nocodb/src/services/tables.service.ts

@ -461,9 +461,7 @@ export class TablesService {
}
if (column.column_name.length > mxColumnLength) {
NcError.badRequest(
`Column name ${column.column_name} exceeds ${mxColumnLength} characters`,
);
column.column_name = column.column_name.slice(0, mxColumnLength);
}
if (column.title && column.title.length > 255) {

6
tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

@ -23,7 +23,11 @@ export class ImportTemplatePage extends BasePage {
const rowCount = await tr.count();
const tableList: string[] = [];
for (let i = 0; i < rowCount; i++) {
const tableName = await getTextExcludeIconText(tr.nth(i));
const tableName = await this.get()
.locator(`.ant-collapse-header`)
.nth(i)
.locator('input[type="text"]')
.inputValue();
tableList.push(tableName);
}
return tableList;

Loading…
Cancel
Save