diff --git a/packages/nc-gui/components/dlg/AirtableImport.vue b/packages/nc-gui/components/dlg/AirtableImport.vue index ef1fdd8fd6..d33078bf37 100644 --- a/packages/nc-gui/components/dlg/AirtableImport.vue +++ b/packages/nc-gui/components/dlg/AirtableImport.vue @@ -54,7 +54,7 @@ const syncSource = ref({ syncLookup: true, syncFormula: false, syncAttachment: true, - syncUsers: true, + syncUsers: false, }, }, }) @@ -210,7 +210,7 @@ async function loadSyncSrc() { syncLookup: true, syncFormula: false, syncAttachment: true, - syncUsers: true, + syncUsers: false, }, }, } @@ -403,22 +403,30 @@ function downloadLogs(filename: string) { - +
- - {{ $t('labels.importUsers') }} - + + + + {{ $t('labels.importFormulaColumns') }} + +
- - - - - {{ $t('labels.importFormulaColumns') }} - - + diff --git a/packages/nc-gui/components/smartsheet/grid/Table.vue b/packages/nc-gui/components/smartsheet/grid/Table.vue index 70bc84c7d2..3371fb33c3 100644 --- a/packages/nc-gui/components/smartsheet/grid/Table.vue +++ b/packages/nc-gui/components/smartsheet/grid/Table.vue @@ -234,6 +234,9 @@ const isKeyDown = ref(false) async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = false) { if (!ctx || !hasEditPermission.value || (!isLinksOrLTAR(fields.value[ctx.col]) && isVirtualCol(fields.value[ctx.col]))) return + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (colMeta.value[ctx.col].isReadonly) return + const rowObj = dataRef.value[ctx.row] const columnObj = fields.value[ctx.col] @@ -424,15 +427,23 @@ const cellMeta = computed(() => { }) }) +const isReadonly = (col: ColumnType) => { + return ( + isSystemColumn(col) || + isLookup(col) || + isRollup(col) || + isFormula(col) || + isVirtualCol(col) || + isCreatedOrLastModifiedTimeCol(col) || + isCreatedOrLastModifiedByCol(col) + ) +} + const colMeta = computed(() => { return fields.value.map((col) => { return { - isLookup: isLookup(col), - isRollup: isRollup(col), - isFormula: isFormula(col), - isCreatedOrLastModifiedTimeCol: isCreatedOrLastModifiedTimeCol(col), - isCreatedOrLastModifiedByCol: isCreatedOrLastModifiedByCol(col), isVirtualCol: isVirtualCol(col), + isReadonly: isReadonly(col), } }) }) @@ -932,6 +943,9 @@ async function clearSelectedRangeOfCells() { continue } + // skip readonly columns + if (isReadonly(col)) continue + row.row[col.title] = null props.push(col.title) } @@ -1251,6 +1265,16 @@ const refreshFillHandle = () => { }) } +const selectedReadonly = computed( + () => + // if all the selected columns are not readonly + (selectedRange.isEmpty() && activeCell.col && colMeta.value[activeCell.col].isReadonly) || + (!selectedRange.isEmpty() && + Array.from({ length: selectedRange.end.col - selectedRange.start.col + 1 }).every( + (_, i) => colMeta.value[selectedRange.start.col + i].isReadonly, + )), +) + const showFillHandle = computed( () => !readOnly.value && @@ -1259,16 +1283,10 @@ const showFillHandle = computed( !dataRef.value[(isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) ?? -1]?.rowMeta?.new && activeCell.col !== null && fields.value[activeCell.col] && - !( - isLookup(fields.value[activeCell.col]) || - isRollup(fields.value[activeCell.col]) || - isFormula(fields.value[activeCell.col]) || - isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col]) || - isCreatedOrLastModifiedByCol(fields.value[activeCell.col]) - ) && !isViewDataLoading.value && !isPaginationLoading.value && - dataRef.value.length, + dataRef.value.length && + !selectedReadonly.value, ) watch( @@ -1660,7 +1678,7 @@ onKeyStroke('ArrowDown', onDown) - + - +
@@ -2241,7 +2246,7 @@ onKeyStroke('ArrowDown', onDown) (isLinksOrLTAR(fields[contextMenuTarget.col]) || !cellMeta[0]?.[contextMenuTarget.col].isVirtualCol) " class="nc-base-menu-item" - :disabled="isSystemColumn(fields[contextMenuTarget.col])" + :disabled="selectedReadonly" data-testid="context-menu-item-clear" @click="clearCell(contextMenuTarget)" > @@ -2255,7 +2260,7 @@ onKeyStroke('ArrowDown', onDown) @@ -2306,8 +2311,8 @@ onKeyStroke('ArrowDown', onDown)
c.title).join(',')); + return primaryKeys.map((c) => row[c.title] ?? row[c.column_name]).join('___'); } diff --git a/packages/nocodb/src/helpers/catchError.ts b/packages/nocodb/src/helpers/catchError.ts index 242e83864e..86bb0e37fb 100644 --- a/packages/nocodb/src/helpers/catchError.ts +++ b/packages/nocodb/src/helpers/catchError.ts @@ -502,6 +502,10 @@ const errorHelpers: { message: (resource: string, id: string) => `${resource} '${id}' not found`, code: 404, }, + [NcErrorType.REQUIRED_FIELD_MISSING]: { + message: (field: string) => `Field '${field}' is required`, + code: 422, + }, [NcErrorType.ERROR_DUPLICATE_RECORD]: { message: (...ids: string[]) => { const isMultiple = Array.isArray(ids) && ids.length > 1; @@ -662,7 +666,22 @@ export class NcError { }); } - static recordNotFound(id: string | string[], args?: NcErrorArgs) { + static recordNotFound( + id: string | string[] | Record | Record[], + args?: NcErrorArgs, + ) { + if (typeof id === 'string') { + id = [id]; + } else if (Array.isArray(id)) { + if (id.every((i) => typeof i === 'string')) { + id = id as string[]; + } else { + id = id.map((i) => Object.values(i).join('___')); + } + } else { + id = Object.values(id).join('___'); + } + throw new NcBaseErrorv2(NcErrorType.RECORD_NOT_FOUND, { params: id, ...args, @@ -676,6 +695,13 @@ export class NcError { }); } + static requiredFieldMissing(field: string, args?: NcErrorArgs) { + throw new NcBaseErrorv2(NcErrorType.REQUIRED_FIELD_MISSING, { + params: field, + ...args, + }); + } + static duplicateRecord(id: string | string[], args?: NcErrorArgs) { throw new NcBaseErrorv2(NcErrorType.ERROR_DUPLICATE_RECORD, { params: id, diff --git a/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts b/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts index c8bed3b305..d0c8bc1b95 100644 --- a/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts +++ b/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import { UITypes } from 'nocodb-sdk'; +import { SqlUiFactory, UITypes } from 'nocodb-sdk'; import Airtable from 'airtable'; import hash from 'object-hash'; import dayjs from 'dayjs'; @@ -200,6 +200,19 @@ export class AtImportProcessor { }; } = {}; + const atFieldAliasToNcFieldAlias = {}; + + const addFieldAlias = (ncTableTitle, atFieldAlias, ncFieldAlias) => { + if (!atFieldAliasToNcFieldAlias[ncTableTitle]) { + atFieldAliasToNcFieldAlias[ncTableTitle] = {}; + } + atFieldAliasToNcFieldAlias[ncTableTitle][atFieldAlias] = ncFieldAlias; + }; + + const getNcFieldAlias = (ncTableTitle, atFieldAlias) => { + return atFieldAliasToNcFieldAlias[ncTableTitle][atFieldAlias]; + }; + const uniqueTableNameGen = getUniqueNameGenerator('sheet'); // run time counter (statistics) @@ -313,7 +326,7 @@ export class AtImportProcessor { rollup: UITypes.Rollup, count: UITypes.Rollup, lookup: UITypes.Lookup, - autoNumber: UITypes.AutoNumber, + autoNumber: UITypes.Decimal, barcode: UITypes.SingleLineText, button: UITypes.Button, }; @@ -519,14 +532,18 @@ export class AtImportProcessor { table.table_name = uniqueTableNameGen(sanitizedName); table.columns = []; + + const source = await Source.get(context, syncDB.sourceId); + + const sqlUi = SqlUiFactory.create({ client: source.type }); + const sysColumns = [ + ...sqlUi.getNewTableColumns().filter((c) => c.column_name === 'id'), { title: ncSysFields.id, column_name: ncSysFields.id, - uidt: UITypes.ID, - meta: { - ag: 'nc', - }, + uidt: UITypes.SingleLineText, + system: true, }, { title: ncSysFields.hash, @@ -536,6 +553,14 @@ export class AtImportProcessor { }, ]; + table.columns.push(...sysColumns); + + for (const sysCol of sysColumns) { + // call to avoid clash with system columns + nc_getSanitizedColumnName(sysCol.title, table.table_name); + addFieldAlias(table.title, sysCol.title, sysCol.title); + } + for (let j = 0; j < tblSchema[i].columns.length; j++) { const col = tblSchema[i].columns[j]; @@ -549,6 +574,9 @@ export class AtImportProcessor { col.name, table.table_name, ); + + addFieldAlias(table.title, col.name, ncName.title); + const ncCol: any = { // Enable to use aTbl identifiers as is: id: col.id, title: ncName.title, @@ -605,8 +633,6 @@ export class AtImportProcessor { } table.columns.push(ncCol); } - table.columns.push(sysColumns[0]); - table.columns.push(sysColumns[1]); tables.push(table); } @@ -639,7 +665,10 @@ export class AtImportProcessor { for (let colIdx = 0; colIdx < table.columns.length; colIdx++) { const aId = aTblSchema[idx].columns.find( (x) => - x.name.trim().replace(/\./g, '_') === table.columns[colIdx].title, + getNcFieldAlias( + table.title, + x.name.trim().replace(/\./g, '_'), + ) === table.columns[colIdx].title, )?.id; if (aId) await sMap.addToMappingTbl( @@ -1398,7 +1427,10 @@ export class AtImportProcessor { // trim spaces on either side of column name // leads to error in NocoDB Object.keys(rec).forEach((key) => { - const replacedKey = key.trim().replace(/\./g, '_'); + const replacedKey = getNcFieldAlias( + table.title, + key.trim().replace(/\./g, '_'), + ); if (key !== replacedKey) { rec[replacedKey] = rec[key]; delete rec[key]; @@ -2556,7 +2588,6 @@ export class AtImportProcessor { data: { error: e.message }, }); logger.log(e); - throw new Error(e.message); } throw e; } @@ -2573,10 +2604,10 @@ const getUniqueNameGenerator = (defaultName = 'name', context = 'default') => { return (initName: string = defaultName): string => { let name = initName === '_' ? defaultName : initName; let c = 0; - while (name in namesRef[finalContext]) { + while (name.toLowerCase() in namesRef[finalContext]) { name = `${initName}_${++c}`; } - namesRef[finalContext][name] = true; + namesRef[finalContext][name.toLowerCase()] = true; return name; }; }; diff --git a/packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts b/packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts index 0bf5339a4b..c267eae0f9 100644 --- a/packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts +++ b/packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts @@ -229,6 +229,7 @@ export async function importData( cookie: {}, skip_hooks: true, foreign_key_checks: !!source.isMeta(), + allowSystemColumn: true, }); logBasic( @@ -275,6 +276,7 @@ export async function importData( cookie: {}, skip_hooks: true, foreign_key_checks: !!source.isMeta(), + allowSystemColumn: true, }); logBasic( @@ -448,6 +450,7 @@ export async function importLTARData( cookie: {}, skip_hooks: true, foreign_key_checks: !!source.isMeta(), + allowSystemColumn: true, }); insertArray = []; @@ -491,6 +494,7 @@ export async function importLTARData( cookie: {}, skip_hooks: true, foreign_key_checks: !!source.isMeta(), + allowSystemColumn: true, }); importedCount += assocTableData[assocMeta.modelMeta.id].length; diff --git a/packages/nocodb/src/services/bulk-data-alias.service.ts b/packages/nocodb/src/services/bulk-data-alias.service.ts index 320d2b89a0..40e455ecb8 100644 --- a/packages/nocodb/src/services/bulk-data-alias.service.ts +++ b/packages/nocodb/src/services/bulk-data-alias.service.ts @@ -48,6 +48,7 @@ export class BulkDataAliasService { foreign_key_checks?: boolean; skip_hooks?: boolean; raw?: boolean; + allowSystemColumn?: boolean; }, ) { return await this.executeBulkOperation(context, { @@ -60,6 +61,7 @@ export class BulkDataAliasService { foreign_key_checks: param.foreign_key_checks, skip_hooks: param.skip_hooks, raw: param.raw, + allowSystemColumn: param.allowSystemColumn, }, ], });