From ea847973819967c389d2fb7cc48041ac21e0557a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 6 Oct 2022 15:16:23 +0530 Subject: [PATCH 01/10] fix(gui): save record only after all required fields are provided Signed-off-by: Pranav C --- packages/nc-gui/composables/useViewData.ts | 10 ++++++++++ packages/nc-gui/utils/columnUtils.ts | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 47102427bb..f5ca81dd36 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -8,6 +8,7 @@ import { extractPkFromRow, extractSdkResponseErrorMsg, getHTMLEncodedText, + isColumnRequiredAndNull, message, ref, useApi, @@ -196,13 +197,22 @@ export function useViewData( async function insertRow(row: Record, rowIndex = formattedData.value?.length) { try { + let allRequiredColumnsProvided = true const insertObj = meta?.value?.columns?.reduce((o: any, col) => { + // check all the required columns are not null + if (isColumnRequiredAndNull(col, row)) { + allRequiredColumnsProvided = false + } + if (!col.ai && row?.[col.title as string] !== null) { o[col.title as string] = row?.[col.title as string] } + return o }, {}) + if (!allRequiredColumnsProvided) return + const insertedData = await $api.dbViewRow.create( NOCO, project?.value.id as string, diff --git a/packages/nc-gui/utils/columnUtils.ts b/packages/nc-gui/utils/columnUtils.ts index 70c07d7827..e6a328928d 100644 --- a/packages/nc-gui/utils/columnUtils.ts +++ b/packages/nc-gui/utils/columnUtils.ts @@ -167,7 +167,11 @@ const getUIDTIcon = (uidt: UITypes | string) => { ).icon } -export { uiTypes, getUIDTIcon } +const isColumnRequiredAndNull = (col: any, row: Record) => { + return col.rqd && (!col.cdf || !col.ai) && (row[col.title!] === undefined || row[col.title!] === null) +} + +export { uiTypes, getUIDTIcon, isColumnRequiredAndNull } /** * @copyright Copyright (c) 2021, Xgene Cloud Ltd From 4f6d92b2e13442e582e460d9fb890aa109a50a24 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 6 Oct 2022 15:16:54 +0530 Subject: [PATCH 02/10] fix(gui): highlight required column if value is null/undefined Signed-off-by: Pranav C --- packages/nc-gui/components/smartsheet/Grid.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index c6c5c963d2..9590122ac5 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -18,6 +18,7 @@ import { createEventHook, extractPkFromRow, inject, + isColumnRequiredAndNull, message, onClickOutside, onMounted, @@ -478,9 +479,10 @@ watch([() => selected.row, () => selected.col], ([row, col]) => { :key="columnObj.id" class="cell relative cursor-pointer nc-grid-cell" :class="{ - active: + 'active': (isUIAllowed('xcDatatableEditable') && selected.col === colIndex && selected.row === rowIndex) || (isUIAllowed('xcDatatableEditable') && selectedRange(rowIndex, colIndex)), + 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), }" :data-key="rowIndex + columnObj.id" :data-col="columnObj.id" @@ -707,4 +709,8 @@ watch([() => selected.row, () => selected.col], ([row, col]) => { tbody tr:hover { @apply bg-gray-100 bg-opacity-50; } + +.nc-required-cell { + box-shadow: inset 0 0 2px #f00; +} From 49ae42e4b4dfc370f048e6704a00099eb7689230 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 6 Oct 2022 15:25:33 +0530 Subject: [PATCH 03/10] refactor(gui): check default value is null or undefined Signed-off-by: Pranav C --- packages/nc-gui/utils/columnUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/utils/columnUtils.ts b/packages/nc-gui/utils/columnUtils.ts index e6a328928d..7d1f027f05 100644 --- a/packages/nc-gui/utils/columnUtils.ts +++ b/packages/nc-gui/utils/columnUtils.ts @@ -1,4 +1,4 @@ -import { UITypes } from 'nocodb-sdk' +import { ColumnType, UITypes } from 'nocodb-sdk' import LinkVariant from '~icons/mdi/link-variant' import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before' import FormatColorText from '~icons/mdi/format-color-text' @@ -167,8 +167,10 @@ const getUIDTIcon = (uidt: UITypes | string) => { ).icon } -const isColumnRequiredAndNull = (col: any, row: Record) => { - return col.rqd && (!col.cdf || !col.ai) && (row[col.title!] === undefined || row[col.title!] === null) +const isColumnRequired = (col: ColumnType) => col.rqd && (col.cdf === null || col.cdf === undefined ) && !col.ai + +const isColumnRequiredAndNull = (col: ColumnType, row: Record) => { + return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null) } export { uiTypes, getUIDTIcon, isColumnRequiredAndNull } From ecdc8ac347918f2eae882e27811ed64eb5f79752 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 6 Oct 2022 16:11:34 +0530 Subject: [PATCH 04/10] fix(gui): update required condition Signed-off-by: Pranav C --- packages/nc-gui/utils/columnUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nc-gui/utils/columnUtils.ts b/packages/nc-gui/utils/columnUtils.ts index 7d1f027f05..64e9206ae5 100644 --- a/packages/nc-gui/utils/columnUtils.ts +++ b/packages/nc-gui/utils/columnUtils.ts @@ -167,7 +167,7 @@ const getUIDTIcon = (uidt: UITypes | string) => { ).icon } -const isColumnRequired = (col: ColumnType) => col.rqd && (col.cdf === null || col.cdf === undefined ) && !col.ai +const isColumnRequired = (col: ColumnType) => col.rqd && !col.cdf && !col.ai const isColumnRequiredAndNull = (col: ColumnType, row: Record) => { return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null) From 73d2154f602b34a34f961045d76b45a1f7e4f695 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Fri, 7 Oct 2022 14:11:18 +0530 Subject: [PATCH 05/10] fix(gui): extract foreign key value from belongs to Signed-off-by: Pranav C --- .../nc-gui/components/smartsheet/Grid.vue | 2 +- packages/nc-gui/composables/useViewData.ts | 70 ++++++++++++++----- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 9590122ac5..cd44b74549 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -511,7 +511,7 @@ watch([() => selected.row, () => selected.col], ([row, col]) => { :row-index="rowIndex" :active="selected.col === colIndex && selected.row === rowIndex" @update:edit-enabled="editEnabled = false" - @save="updateOrSaveRow(row, columnObj.title)" + @save="updateOrSaveRow(row, columnObj.title, state)" @navigate="onNavigate" @cancel="editEnabled = false" /> diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index f5ca81dd36..0dde08157e 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -1,5 +1,14 @@ -import { ViewTypes } from 'nocodb-sdk' -import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' +import { RelationTypes, UITypes, ViewTypes } from 'nocodb-sdk' +import type { + Api, + ColumnType, + FormType, + GalleryType, + LinkToAnotherRecordType, + PaginatedType, + TableType, + ViewType, +} from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' import { IsPublicInj, @@ -20,6 +29,7 @@ import { useSmartsheetStoreOrThrow, useUIPermission, } from '#imports' +import { useMetas } from '~/composables/useMetas' import type { Row } from '~/lib' const formatData = (list: Row[]) => @@ -41,6 +51,7 @@ export function useViewData( const { t } = useI18n() const { api, isLoading, error } = useApi() + const { metas, getMeta } = useMetas() const { appInfo } = $(useGlobal()) const appInfoDefaultLimit = appInfo.defaultLimit || 25 @@ -175,12 +186,12 @@ export function useViewData( if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return const response = !isPublic.value ? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, { - ...queryParams.value, - ...params, - ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), - ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), - where: where?.value, - }) + ...queryParams.value, + ...params, + ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), + ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), + where: where?.value, + }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) formattedData.value = formatData(response.list) paginationData.value = response.pageInfo @@ -195,13 +206,35 @@ export function useViewData( galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id) } - async function insertRow(row: Record, rowIndex = formattedData.value?.length) { + async function insertRow( + row: Record, + rowIndex = formattedData.value?.length, + ltarState?: Record = {}, + ) { try { - let allRequiredColumnsProvided = true - const insertObj = meta?.value?.columns?.reduce((o: any, col) => { + const missingRequiredColumns = new Set() + const insertObj = await meta?.value?.columns?.reduce(async (_o: Promise, col) => { + const o = await _o + if ( + ltarState && + col.uidt === UITypes.LinkToAnotherRecord && + (col.colOptions).type === RelationTypes.BELONGS_TO + ) { + if (ltarState[col.title!]) { + const colOpt = col.colOptions + const childCol = meta.value!.columns!.find((c) => colOpt.fk_child_column_id === c.id) + const relatedTableMeta = (await getMeta(colOpt.fk_related_model_id!)) as TableType + if (relatedTableMeta && childCol) { + o[childCol.title!] = + ltarState[col.title!][relatedTableMeta!.columns!.find((c) => c.id === colOpt.fk_parent_column_id)!.title!] + missingRequiredColumns.delete(childCol.title) + } + } + } + // check all the required columns are not null if (isColumnRequiredAndNull(col, row)) { - allRequiredColumnsProvided = false + missingRequiredColumns.add(col.title) } if (!col.ai && row?.[col.title as string] !== null) { @@ -209,9 +242,9 @@ export function useViewData( } return o - }, {}) + }, Promise.resolve({})) - if (!allRequiredColumnsProvided) return + if (missingRequiredColumns.size) return const insertedData = await $api.dbViewRow.create( NOCO, @@ -261,7 +294,8 @@ export function useViewData( value: getHTMLEncodedText(toUpdate.row[property]), prev_value: getHTMLEncodedText(toUpdate.oldRow[property]), }) - .then(() => {}) + .then(() => { + }) /** update row data(to sync formula and other related columns) */ Object.assign(toUpdate.row, updatedRowData) @@ -271,9 +305,9 @@ export function useViewData( } } - async function updateOrSaveRow(row: Row, property?: string) { + async function updateOrSaveRow(row: Row, property?: string, ltarState?: Record) { if (row.rowMeta.new) { - return await insertRow(row.row, formattedData.value.indexOf(row)) + return await insertRow(row.row, formattedData.value.indexOf(row), ltarState) } else { await updateRowProperty(row, property!) } @@ -290,7 +324,7 @@ export function useViewData( async function deleteRowById(id: string) { if (!id) { - throw new Error("Delete not allowed for table which doesn't have primary Key") + throw new Error('Delete not allowed for table which doesn\'t have primary Key') } const res: any = await $api.dbViewRow.delete( From c93e2afda0d10d9193c5d72a7b0a517a21478b0c Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 8 Oct 2022 14:13:09 +0530 Subject: [PATCH 06/10] feat(gui): save expanded form data only if all required field values provided Signed-off-by: Pranav C --- .../smartsheet/expanded-form/Header.vue | 4 +- .../composables/useExpandedFormStore.ts | 84 +++--- packages/nc-gui/composables/useViewData.ts | 67 +---- packages/nc-gui/utils/columnUtils.ts | 5 +- packages/nc-gui/utils/dataUtils.ts | 51 +++- packages/nocodb-sdk/package-lock.json | 281 ++++++++++++++++++ 6 files changed, 399 insertions(+), 93 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue index bd7bc1c2de..b21c2d2d5c 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue @@ -19,7 +19,7 @@ const { meta, isSqlView } = useSmartsheetStoreOrThrow() const { commentsDrawer, primaryValue, primaryKey, save: _save, loadRow } = useExpandedFormStoreOrThrow() -const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow() +const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow() const { isUIAllowed } = useUIPermission() @@ -27,7 +27,7 @@ const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const save = async () => { if (isNew.value) { - const data = await _save() + const data = await _save(state.value) await syncLTARRefs(data) reloadTrigger?.trigger() } else { diff --git a/packages/nc-gui/composables/useExpandedFormStore.ts b/packages/nc-gui/composables/useExpandedFormStore.ts index 69c869bcf8..6e9dad1e48 100644 --- a/packages/nc-gui/composables/useExpandedFormStore.ts +++ b/packages/nc-gui/composables/useExpandedFormStore.ts @@ -19,7 +19,9 @@ import { useProvideSmartsheetRowStore, useSharedView, } from '#imports' +import { useMetas } from '~/composables/useMetas' import type { Row } from '~/lib' +import { populateInsertObject } from '~/utils' const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref, row: Ref) => { const { $e, $state, $api } = useNuxtApp() @@ -132,53 +134,65 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m $e('a:row-expand:comment') } - const save = async () => { + const save = async (ltarState: Record = {}) => { let data try { - const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => { - obj[col] = row.value.row[col] - return obj - }, {} as Record) - const isNewRow = row.value.rowMeta?.new ?? false if (isNewRow) { - data = await $api.dbTableRow.create('noco', project.value.title as string, meta.value.title, updateOrInsertObj) + const { getMeta } = useMetas() + + const { missingRequiredColumns, insertObj } = await populateInsertObject({ + meta: meta.value, + ltarState, + getMeta, + row: row.value.row, + }) + + if (missingRequiredColumns.size) return + + data = await $api.dbTableRow.create('noco', project.value.title as string, meta.value.title, insertObj) Object.assign(row.value, { row: data, rowMeta: {}, oldRow: { ...data }, }) - } else if (Object.keys(updateOrInsertObj).length) { - const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) - - if (!id) { - return message.info("Update not allowed for table which doesn't have primary Key") - } - - await $api.dbTableRow.update(NOCO, project.value.title as string, meta.value.title, id, updateOrInsertObj) - - for (const key of Object.keys(updateOrInsertObj)) { - // audit - $api.utils - .auditRowUpdate(id, { - fk_model_id: meta.value.id, - column_name: key, - row_id: id, - value: getHTMLEncodedText(updateOrInsertObj[key]), - prev_value: getHTMLEncodedText(row.value.oldRow[key]), - }) - .then(async () => { - /** load latest comments/audit if right drawer is open */ - if (commentsDrawer.value) { - await loadCommentsAndLogs() - } - }) - } } else { - // No columns to update - return message.info(t('msg.info.noColumnsToUpdate')) + const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => { + obj[col] = row.value.row[col] + return obj + }, {} as Record) + if (Object.keys(updateOrInsertObj).length) { + const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) + + if (!id) { + return message.info("Update not allowed for table which doesn't have primary Key") + } + + await $api.dbTableRow.update(NOCO, project.value.title as string, meta.value.title, id, updateOrInsertObj) + + for (const key of Object.keys(updateOrInsertObj)) { + // audit + $api.utils + .auditRowUpdate(id, { + fk_model_id: meta.value.id, + column_name: key, + row_id: id, + value: getHTMLEncodedText(updateOrInsertObj[key]), + prev_value: getHTMLEncodedText(row.value.oldRow[key]), + }) + .then(async () => { + /** load latest comments/audit if right drawer is open */ + if (commentsDrawer.value) { + await loadCommentsAndLogs() + } + }) + } + } else { + // No columns to update + return message.info(t('msg.info.noColumnsToUpdate')) + } } if (activeView.value?.type === ViewTypes.KANBAN) { diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 0dde08157e..3d05bb2a15 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -1,14 +1,5 @@ -import { RelationTypes, UITypes, ViewTypes } from 'nocodb-sdk' -import type { - Api, - ColumnType, - FormType, - GalleryType, - LinkToAnotherRecordType, - PaginatedType, - TableType, - ViewType, -} from 'nocodb-sdk' +import { ViewTypes } from 'nocodb-sdk' +import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' import { IsPublicInj, @@ -17,8 +8,8 @@ import { extractPkFromRow, extractSdkResponseErrorMsg, getHTMLEncodedText, - isColumnRequiredAndNull, message, + populateInsertObject, ref, useApi, useGlobal, @@ -51,7 +42,6 @@ export function useViewData( const { t } = useI18n() const { api, isLoading, error } = useApi() - const { metas, getMeta } = useMetas() const { appInfo } = $(useGlobal()) const appInfoDefaultLimit = appInfo.defaultLimit || 25 @@ -186,12 +176,12 @@ export function useViewData( if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return const response = !isPublic.value ? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, { - ...queryParams.value, - ...params, - ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), - ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), - where: where?.value, - }) + ...queryParams.value, + ...params, + ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), + ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), + where: where?.value, + }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) formattedData.value = formatData(response.list) paginationData.value = response.pageInfo @@ -209,40 +199,12 @@ export function useViewData( async function insertRow( row: Record, rowIndex = formattedData.value?.length, - ltarState?: Record = {}, + ltarState: Record = {}, ) { try { - const missingRequiredColumns = new Set() - const insertObj = await meta?.value?.columns?.reduce(async (_o: Promise, col) => { - const o = await _o - if ( - ltarState && - col.uidt === UITypes.LinkToAnotherRecord && - (col.colOptions).type === RelationTypes.BELONGS_TO - ) { - if (ltarState[col.title!]) { - const colOpt = col.colOptions - const childCol = meta.value!.columns!.find((c) => colOpt.fk_child_column_id === c.id) - const relatedTableMeta = (await getMeta(colOpt.fk_related_model_id!)) as TableType - if (relatedTableMeta && childCol) { - o[childCol.title!] = - ltarState[col.title!][relatedTableMeta!.columns!.find((c) => c.id === colOpt.fk_parent_column_id)!.title!] - missingRequiredColumns.delete(childCol.title) - } - } - } + const { getMeta } = useMetas() - // check all the required columns are not null - if (isColumnRequiredAndNull(col, row)) { - missingRequiredColumns.add(col.title) - } - - if (!col.ai && row?.[col.title as string] !== null) { - o[col.title as string] = row?.[col.title as string] - } - - return o - }, Promise.resolve({})) + const { missingRequiredColumns, insertObj } = await populateInsertObject({ meta, ltarState, getMeta, row }) if (missingRequiredColumns.size) return @@ -294,8 +256,7 @@ export function useViewData( value: getHTMLEncodedText(toUpdate.row[property]), prev_value: getHTMLEncodedText(toUpdate.oldRow[property]), }) - .then(() => { - }) + .then(() => {}) /** update row data(to sync formula and other related columns) */ Object.assign(toUpdate.row, updatedRowData) @@ -324,7 +285,7 @@ export function useViewData( async function deleteRowById(id: string) { if (!id) { - throw new Error('Delete not allowed for table which doesn\'t have primary Key') + throw new Error("Delete not allowed for table which doesn't have primary Key") } const res: any = await $api.dbViewRow.delete( diff --git a/packages/nc-gui/utils/columnUtils.ts b/packages/nc-gui/utils/columnUtils.ts index 64e9206ae5..915968c28f 100644 --- a/packages/nc-gui/utils/columnUtils.ts +++ b/packages/nc-gui/utils/columnUtils.ts @@ -1,4 +1,5 @@ -import { ColumnType, UITypes } from 'nocodb-sdk' +import type { ColumnType } from 'nocodb-sdk' +import { UITypes } from 'nocodb-sdk' import LinkVariant from '~icons/mdi/link-variant' import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before' import FormatColorText from '~icons/mdi/format-color-text' @@ -170,7 +171,7 @@ const getUIDTIcon = (uidt: UITypes | string) => { const isColumnRequired = (col: ColumnType) => col.rqd && !col.cdf && !col.ai const isColumnRequiredAndNull = (col: ColumnType, row: Record) => { - return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null) + return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null) // && isVirtualColRequired() } export { uiTypes, getUIDTIcon, isColumnRequiredAndNull } diff --git a/packages/nc-gui/utils/dataUtils.ts b/packages/nc-gui/utils/dataUtils.ts index 290d445aca..e57f566db8 100644 --- a/packages/nc-gui/utils/dataUtils.ts +++ b/packages/nc-gui/utils/dataUtils.ts @@ -1,4 +1,5 @@ -import type { ColumnType } from 'nocodb-sdk' +import { RelationTypes, UITypes } from 'nocodb-sdk' +import type { ColumnType, LinkToAnotherRecordType, TableInfoType, TableType } from 'nocodb-sdk' export const extractPkFromRow = (row: Record, columns: ColumnType[]) => { return ( @@ -9,3 +10,51 @@ export const extractPkFromRow = (row: Record, columns: ColumnType[] .join('___') ) } + +// a function to populate insert object and verify if all required fields are present +export async function populateInsertObject({ + getMeta, + row, + meta, + ltarState, +}: { + meta: TableType + ltarState: Record + getMeta: (tableIdOrTitle: string, force?: boolean) => Promise + row: Record +}) { + const missingRequiredColumns = new Set() + const insertObj = await meta.columns?.reduce(async (_o: Promise, col) => { + const o = await _o + + // if column is BT relation then check if foreign key is not_null(required) + if ( + ltarState && + col.uidt === UITypes.LinkToAnotherRecord && + (col.colOptions).type === RelationTypes.BELONGS_TO + ) { + if (ltarState[col.title!]) { + const colOpt = col.colOptions + const childCol = meta.columns!.find((c) => colOpt.fk_child_column_id === c.id) + const relatedTableMeta = (await getMeta(colOpt.fk_related_model_id!)) as TableType + if (relatedTableMeta && childCol) { + o[childCol.title!] = + ltarState[col.title!][relatedTableMeta!.columns!.find((c) => c.id === colOpt.fk_parent_column_id)!.title!] + missingRequiredColumns.delete(childCol.title) + } + } + } + // check all the required columns are not null + if (isColumnRequiredAndNull(col, row)) { + missingRequiredColumns.add(col.title) + } + + if (!col.ai && row?.[col.title as string] !== null) { + o[col.title as string] = row?.[col.title as string] + } + + return o + }, Promise.resolve({})) + + return { missingRequiredColumns, insertObj } +} diff --git a/packages/nocodb-sdk/package-lock.json b/packages/nocodb-sdk/package-lock.json index 4e7656e0e1..497bd8f603 100644 --- a/packages/nocodb-sdk/package-lock.json +++ b/packages/nocodb-sdk/package-lock.json @@ -538,6 +538,19 @@ "node": ">=8" } }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/table/node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -1565,6 +1578,18 @@ "node": ">=10" } }, + "node_modules/gh-pages/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/escape-goat": { "version": "2.1.1", "dev": true, @@ -1786,6 +1811,15 @@ "node": ">=0.10.0" } }, + "node_modules/nyc/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/npm-run-all/node_modules/path-key": { "version": "2.0.1", "dev": true, @@ -2106,6 +2140,15 @@ "node": ">= 8" } }, + "node_modules/gh-pages/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "dev": true, @@ -2539,6 +2582,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "dev": true, @@ -2970,6 +3022,18 @@ "node": ">=8" } }, + "node_modules/git-semver-tags/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "dev": true, @@ -4094,6 +4158,15 @@ "node": ">=0.8.0" } }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/dotgitignore/node_modules/path-exists": { "version": "3.0.0", "dev": true, @@ -5144,6 +5217,15 @@ "once": "^1.4.0" } }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "dev": true, @@ -5160,6 +5242,21 @@ "node": ">=4.0.0" } }, + "node_modules/standard-version/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-module-utils/node_modules/p-limit": { "version": "1.3.0", "dev": true, @@ -5596,6 +5693,15 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/standard-version/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -7101,6 +7207,18 @@ "node": ">=4.8" } }, + "node_modules/standard-version/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "dev": true, @@ -7133,6 +7251,18 @@ "dev": true, "license": "MIT" }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "dev": true, @@ -7210,6 +7340,21 @@ "license": "MIT", "optional": true }, + "node_modules/standard-version/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "dev": true, @@ -7463,6 +7608,21 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/standard-version/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "dev": true, @@ -8451,6 +8611,15 @@ "ini": "^1.3.2" } }, + "node_modules/standard-version/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/semver-diff": { "version": "3.1.1", "dev": true, @@ -10468,6 +10637,12 @@ "version": "2.1.0", "dev": true }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true + }, "array.prototype.flat": { "version": "1.2.5", "dev": true, @@ -12614,6 +12789,15 @@ "globby": "^6.1.0" }, "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, "commander": { "version": "2.20.3", "dev": true @@ -12641,6 +12825,12 @@ "pify": { "version": "2.3.0", "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true } } }, @@ -12737,6 +12927,12 @@ "semver": { "version": "6.3.0", "dev": true + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true } } }, @@ -13104,6 +13300,12 @@ "version": "3.0.0", "dev": true }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, "restore-cursor": { "version": "2.0.0", "dev": true, @@ -14064,6 +14266,12 @@ "color-convert": "^2.0.1" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "cliui": { "version": "6.0.0", "dev": true, @@ -14105,6 +14313,16 @@ "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, @@ -15042,6 +15260,12 @@ "supports-color": "^5.3.0" } }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, "find-up": { "version": "5.0.0", "dev": true, @@ -15049,6 +15273,48 @@ "locate-path": "^6.0.0", "path-exists": "^4.0.0" } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -15528,6 +15794,15 @@ "shiki": "^0.10.1" }, "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "glob": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", @@ -15804,6 +16079,12 @@ "yn": { "version": "3.1.1", "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } \ No newline at end of file From 491f9557a3cc77a518e08c382f43da382c7e534e Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 8 Oct 2022 16:10:47 +0530 Subject: [PATCH 07/10] feat(gui): if bt column is required then mark it as required in header Signed-off-by: Pranav C --- .../components/smartsheet/header/VirtualCell.vue | 3 ++- .../nc-gui/composables/useExpandedFormStore.ts | 1 + packages/nc-gui/composables/useViewData.ts | 7 ++++++- packages/nc-gui/utils/columnUtils.ts | 14 +++++++++----- packages/nc-gui/utils/dataUtils.ts | 6 ++++++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/header/VirtualCell.vue b/packages/nc-gui/components/smartsheet/header/VirtualCell.vue index 1740c31a42..f0dc511883 100644 --- a/packages/nc-gui/components/smartsheet/header/VirtualCell.vue +++ b/packages/nc-gui/components/smartsheet/header/VirtualCell.vue @@ -7,6 +7,7 @@ import { MetaInj, computed, inject, + isVirtualColRequired, provide, ref, toRef, @@ -108,7 +109,7 @@ const tooltipMsg = computed(() => { {{ column.title }} -  * +  *