From 479373fe04ce8a07ff34a989cd0116a0b75fa7a4 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 15 Sep 2022 13:42:24 +0530 Subject: [PATCH 001/164] refactor: i18n validation error messages Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../nc-gui/components/dlg/AirtableImport.vue | 4 +- .../nc-gui/components/dlg/QuickImport.vue | 4 +- .../nc-gui/components/template/Editor.vue | 8 +- packages/nc-gui/components/webhook/Editor.vue | 24 +++--- packages/nc-gui/lang/en.json | 15 +++- .../pages/index/index/create-external.vue | 16 ++-- packages/nc-gui/utils/validation.ts | 74 +++++++++++++------ 7 files changed, 93 insertions(+), 52 deletions(-) diff --git a/packages/nc-gui/components/dlg/AirtableImport.vue b/packages/nc-gui/components/dlg/AirtableImport.vue index 0aeca558e4..f5278df1cf 100644 --- a/packages/nc-gui/components/dlg/AirtableImport.vue +++ b/packages/nc-gui/components/dlg/AirtableImport.vue @@ -65,8 +65,8 @@ const syncSource = ref({ }) const validators = computed(() => ({ - 'details.apiKey': [fieldRequiredValidator], - 'details.syncSourceUrlOrId': [fieldRequiredValidator], + 'details.apiKey': [fieldRequiredValidator()], + 'details.syncSourceUrlOrId': [fieldRequiredValidator()], })) const dialogShow = computed({ diff --git a/packages/nc-gui/components/dlg/QuickImport.vue b/packages/nc-gui/components/dlg/QuickImport.vue index 4d98ffbff2..d7e2567063 100644 --- a/packages/nc-gui/components/dlg/QuickImport.vue +++ b/packages/nc-gui/components/dlg/QuickImport.vue @@ -70,8 +70,8 @@ const isImportTypeCsv = computed(() => importType === 'csv') const IsImportTypeExcel = computed(() => importType === 'excel') const validators = computed(() => ({ - url: [fieldRequiredValidator, importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator], - maxRowsToParse: [fieldRequiredValidator], + url: [fieldRequiredValidator(), importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator], + maxRowsToParse: [fieldRequiredValidator()], })) const { validate, validateInfos } = useForm(importState, validators) diff --git a/packages/nc-gui/components/template/Editor.vue b/packages/nc-gui/components/template/Editor.vue index fd347ddef1..33aa8ac15e 100644 --- a/packages/nc-gui/components/template/Editor.vue +++ b/packages/nc-gui/components/template/Editor.vue @@ -94,13 +94,13 @@ const data = reactive<{ title: string | null; name: string; tables: (TableType & }) const validators = computed(() => - data.tables.reduce>((acc, table, tableIdx) => { - acc[`tables.${tableIdx}.ref_table_name`] = [fieldRequiredValidator] + data.tables.reduce]>>((acc, table, tableIdx) => { + acc[`tables.${tableIdx}.ref_table_name`] = [fieldRequiredValidator()] hasSelectColumn.value[tableIdx] = false table.columns?.forEach((column, columnIdx) => { - acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator] - acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator] + acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator()] + acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()] if (isSelect(column)) { hasSelectColumn.value[tableIdx] = true } diff --git a/packages/nc-gui/components/webhook/Editor.vue b/packages/nc-gui/components/webhook/Editor.vue index 9abdf91bc0..eb43e8c656 100644 --- a/packages/nc-gui/components/webhook/Editor.vue +++ b/packages/nc-gui/components/webhook/Editor.vue @@ -184,28 +184,28 @@ const methodList = ref([ const validators = computed(() => { return { - 'title': [fieldRequiredValidator], - 'eventOperation': [fieldRequiredValidator], - 'notification.type': [fieldRequiredValidator], + 'title': [fieldRequiredValidator()], + 'eventOperation': [fieldRequiredValidator()], + 'notification.type': [fieldRequiredValidator()], ...(hook.notification.type === 'URL' && { - 'notification.payload.method': [fieldRequiredValidator], - 'notification.payload.path': [fieldRequiredValidator], + 'notification.payload.method': [fieldRequiredValidator()], + 'notification.payload.path': [fieldRequiredValidator()], }), ...(hook.notification.type === 'Email' && { - 'notification.payload.to': [fieldRequiredValidator], - 'notification.payload.subject': [fieldRequiredValidator], - 'notification.payload.body': [fieldRequiredValidator], + 'notification.payload.to': [fieldRequiredValidator()], + 'notification.payload.subject': [fieldRequiredValidator()], + 'notification.payload.body': [fieldRequiredValidator()], }), ...((hook.notification.type === 'Slack' || hook.notification.type === 'Microsoft Teams' || hook.notification.type === 'Discord' || hook.notification.type === 'Mattermost') && { - 'notification.payload.channels': [fieldRequiredValidator], - 'notification.payload.body': [fieldRequiredValidator], + 'notification.payload.channels': [fieldRequiredValidator()], + 'notification.payload.body': [fieldRequiredValidator()], }), ...((hook.notification.type === 'Twilio' || hook.notification.type === 'Whatsapp Twilio') && { - 'notification.payload.body': [fieldRequiredValidator], - 'notification.payload.to': [fieldRequiredValidator], + 'notification.payload.body': [fieldRequiredValidator()], + 'notification.payload.to': [fieldRequiredValidator()], }), } }) diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index 04feb6edcd..8def53b7e9 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -616,7 +616,20 @@ "rowUpdateFailed": "Row update failed", "deleteRowFailed": "Failed to delete row", "setFormDataFailed": "Failed to set form data", - "formViewUpdateFailed": "Failed to update form view" + "formViewUpdateFailed": "Failed to update form view", + "tableNameRequired": "Table name is required", + "nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _", + "followingCharactersAreNotAllowed": "Following characters are not allowed", + "columnNameRequired": "Column name is required", + "projectNameExceeds50Characters": "Project name exceeds 50 characters", + "projectNameCannotStartWithSpace": "Project name cannot start with space", + "requiredField": "Required field", + "ipNotAllowed": "IP not allowed", + "targetFileIsNotAnAcceptedFileType": "Target file is not an accepted file type", + "theAcceptedFileTypeIsCsv": "The accepted file type is .csv", + "theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots", + "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", + "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed" }, "toast": { "exportMetadata": "Project metadata exported successfully", diff --git a/packages/nc-gui/pages/index/index/create-external.vue b/packages/nc-gui/pages/index/index/create-external.vue index 4d696df589..64a9f801b4 100644 --- a/packages/nc-gui/pages/index/index/create-external.vue +++ b/packages/nc-gui/pages/index/index/create-external.vue @@ -73,20 +73,20 @@ const validators = computed(() => { projectTitleValidator, ], 'extraParameters': [extraParameterValidator], - 'dataSource.client': [fieldRequiredValidator], + 'dataSource.client': [fieldRequiredValidator()], ...(formState.dataSource.client === ClientType.SQLITE ? { - 'dataSource.connection.connection.filename': [fieldRequiredValidator], + 'dataSource.connection.connection.filename': [fieldRequiredValidator()], } : { - 'dataSource.connection.host': [fieldRequiredValidator], - 'dataSource.connection.port': [fieldRequiredValidator], - 'dataSource.connection.user': [fieldRequiredValidator], - 'dataSource.connection.password': [fieldRequiredValidator], - 'dataSource.connection.database': [fieldRequiredValidator], + 'dataSource.connection.host': [fieldRequiredValidator()], + 'dataSource.connection.port': [fieldRequiredValidator()], + 'dataSource.connection.user': [fieldRequiredValidator()], + 'dataSource.connection.password': [fieldRequiredValidator()], + 'dataSource.connection.database': [fieldRequiredValidator()], ...([ClientType.PG, ClientType.MSSQL].includes(formState.dataSource.client) ? { - 'dataSource.searchPath.0': [fieldRequiredValidator], + 'dataSource.searchPath.0': [fieldRequiredValidator()], } : {}), }), diff --git a/packages/nc-gui/utils/validation.ts b/packages/nc-gui/utils/validation.ts index a5e63464a8..3b0a9b893b 100644 --- a/packages/nc-gui/utils/validation.ts +++ b/packages/nc-gui/utils/validation.ts @@ -1,9 +1,14 @@ +import { getI18n } from '~/plugins/a.i18n' + export const isEmail = (v: string) => /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(v) export function validateTableName(v: string, isGQL = false) { + const { t } = getI18n().global + if (!v) { - return 'Table name required' + // return 'Table name required' + return t('msg.error.tableNameRequired') } // GraphQL naming convention @@ -15,11 +20,13 @@ export function validateTableName(v: string, isGQL = false) { } if (/^[^_A-Za-z]/.test(v)) { - return 'Name should start with an alphabet or _' + // return 'Name should start with an alphabet or _' + return t('msg.error.nameShouldStartWithAnAlphabetOr_') } const m = v.match(/[^_A-Za-z\d]/g) if (m) { - return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + // return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + return `${t('msg.error.followingCharactersAreNotAllowed')} ${m.map((c) => JSON.stringify(c)).join(', ')}` } } else { // exclude . / \ @@ -27,7 +34,8 @@ export function validateTableName(v: string, isGQL = false) { // 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. const m = v.match(/[./\\]/g) if (m) { - return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + // return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + return `${t('msg.error.followingCharactersAreNotAllowed')} ${m.map((c) => JSON.stringify(c)).join(', ')}` } return true @@ -35,8 +43,10 @@ export function validateTableName(v: string, isGQL = false) { } export function validateColumnName(v: string, isGQL = false) { + const { t } = getI18n().global if (!v) { - return 'Column name required' + // return 'Column name required' + return t('msg.error.columnNameRequired') } // GraphQL naming convention @@ -47,11 +57,13 @@ export function validateColumnName(v: string, isGQL = false) { } if (/^[^_A-Za-z]/.test(v)) { - return 'Name should start with an alphabet or _' + // return 'Name should start with an alphabet or _' + return t('msg.error.nameShouldStartWithAnAlphabetOr_') } const m = v.match(/[^_A-Za-z\d]/g) if (m) { - return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + // return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + return `${t('msg.error.followingCharactersAreNotAllowed')} ${m.map((c) => JSON.stringify(c)).join(', ')}` } } else { // exclude . / \ @@ -59,7 +71,8 @@ export function validateColumnName(v: string, isGQL = false) { // 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. const m = v.match(/[./\\]/g) if (m) { - return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + // return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` + return `${t('msg.error.followingCharactersAreNotAllowed')} ${m.map((c) => JSON.stringify(c)).join(', ')}` } return true @@ -68,36 +81,40 @@ export function validateColumnName(v: string, isGQL = false) { export const projectTitleValidator = { validator: (rule: any, value: any, callback: (errMsg?: string) => void) => { + const { t } = getI18n().global if (value?.length > 50) { - callback('Project name exceeds 50 characters') + // callback('Project name exceeds 50 characters') + callback(t('msg.error.projectNameExceeds50Characters')) } if (value[0] === ' ') { - callback('Project name cannot start with space') + // callback('Project name cannot start with space') + callback(t('msg.error.projectNameCannotStartWithSpace')) } callback() }, } -export const fieldRequiredValidator = { - required: true, - message: 'Field is required', +export const fieldRequiredValidator = () => { + const { t } = getI18n().global + return { + required: true, + // message: `Required field`, + message: t('msg.error.requiredField'), + } } -export const getRequiredValidator = (field = 'Field') => ({ - required: true, - message: `${field} is required`, -}) - export const importUrlValidator = { validator: (rule: any, value: any) => { return new Promise((resolve, reject) => { + const { t } = getI18n().global if ( /(10)(\.([2]([0-5][0-5]|[01234][6-9])|[1][0-9][0-9]|[1-9][0-9]|[0-9])){3}|(172)\.(1[6-9]|2[0-9]|3[0-1])(\.(2[0-4][0-9]|25[0-5]|[1][0-9][0-9]|[1-9][0-9]|[0-9])){2}|(192)\.(168)(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){2}|(0.0.0.0)|localhost?/g.test( value, ) ) { - return reject(new Error('IP Not allowed!')) + // return reject(new Error('IP Not allowed!')) + return reject(new Error(t('msg.error.ipNotAllowed'))) } return resolve(true) }) @@ -107,8 +124,12 @@ export const importUrlValidator = { export const importCsvUrlValidator = { validator: (rule: any, value: any) => { return new Promise((resolve, reject) => { + const { t } = getI18n().global if (value && !/.*\.(csv)/.test(value)) { - return reject(new Error('Target file is not an accepted file type. The accepted file type is .csv!')) + // return reject(new Error('Target file is not an accepted file type. The accepted file type is .csv!')) + return reject( + new Error(`${t('msg.error.targetFileIsNotAnAcceptedFileType')}. ${t('msg.error.theAcceptedFileTypeIsCsv')}`), + ) } return resolve(true) }) @@ -118,9 +139,13 @@ export const importCsvUrlValidator = { export const importExcelUrlValidator = { validator: (rule: any, value: any) => { return new Promise((resolve, reject) => { + const { t } = getI18n().global if (value && !/.*\.(xls|xlsx|xlsm|ods|ots)/.test(value)) { return reject( - new Error('Target file is not an accepted file type. The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots!'), + // new Error('Target file is not an accepted file type. The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots!'), + new Error( + `${t('msg.error.targetFileIsNotAnAcceptedFileType')}. ${t('msg.error.theAcceptedFileTypesAreXlsXlsxXlsmOdsOts')}`, + ), ) } return resolve(true) @@ -131,12 +156,15 @@ export const importExcelUrlValidator = { export const extraParameterValidator = { validator: (_: unknown, value: { key: string; value: string }[]) => { return new Promise((resolve, reject) => { + const { t } = getI18n().global for (const param of value) { if (param.key === '') { - return reject(new Error('Parameter key cannot be empty')) + // return reject(new Error('Parameter key cannot be empty')) + return reject(new Error(t('msg.error.parameterKeyCannotBeEmpty'))) } if (value.filter((el: any) => el.key === param.key).length !== 1) { - return reject(new Error('Duplicate parameter keys are not allowed')) + // return reject(new Error('Duplicate parameter keys are not allowed')) + return reject(new Error(t('msg.error.duplicateParameterKeysAreNotAllowed'))) } } return resolve(true) From 64199342554c9f98406e4182a1787ddb0caae28a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 14 Sep 2022 14:10:42 +0530 Subject: [PATCH 002/164] wip(gui): url for expanded row Signed-off-by: Pranav C --- .../nc-gui/components/smartsheet/Grid.vue | 50 +++++++++++++++++-- .../smartsheet/expanded-form/index.vue | 7 +++ .../composables/useExpandedFormStore.ts | 5 +- .../[[rowId]].vue} | 0 4 files changed, 56 insertions(+), 6 deletions(-) rename packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/{[[viewTitle]].vue => [[viewTitle]]/[[rowId]].vue} (100%) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 515ce9ad41..73546a870d 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -2,6 +2,7 @@ import type { ColumnType } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk' import { message } from 'ant-design-vue' +import { useRoute } from '#app' import { ActiveViewInj, CellUrlDisableOverlayInj, @@ -34,6 +35,7 @@ import { } from '#imports' import type { Row } from '~/composables' import { NavigateDir } from '~/lib' +import { extractPkFromRow } from '~/utils' const { t } = useI18n() @@ -53,6 +55,9 @@ const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook() const { isUIAllowed } = useUIPermission() const hasEditPermission = isUIAllowed('xcDatatableEditable') +const route = useRoute() +const router = useRouter() + // todo: get from parent ( inject or use prop ) const isView = false @@ -130,10 +135,23 @@ reloadViewDataHook?.on(async (shouldShowLoading) => { const skipRowRemovalOnCancel = ref(false) const expandForm = (row: Row, state?: Record, fromToolbar = false) => { - expandedFormRow.value = row - expandedFormRowState.value = state - expandedFormDlg.value = true - skipRowRemovalOnCancel.value = !fromToolbar + const rowId = extractPkFromRow(row.row, meta.value.columns) + + if (rowId) { + router.push({ + params: { + ...route.params, + rowId, + }, + }) + } else { + expandedFormRow.value = row + expandedFormRowState.value = state + expandedFormDlg.value = true + skipRowRemovalOnCancel.value = !fromToolbar + } + + /* */ } openNewRecordFormHook?.on(async () => { @@ -377,6 +395,21 @@ onBeforeUnmount(async () => { } } }) + +const expandedFormOnRowIdDlg = computed({ + get() { + return !!route.params.rowId + }, + set(val) { + if (!val) + router.push({ + params: { + ...route.params, + rowId: undefined, + }, + }) + }, +}) diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index e2e535bd4d..3e1ccaf8f2 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -29,6 +29,7 @@ interface Props { meta: TableType loadRow?: boolean useMetaFields?: boolean + rowId?: string } const props = defineProps() @@ -52,10 +53,16 @@ provide(MetaInj, meta) const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row) + if (props.loadRow) { await loadRow() } +if (props.rowId) { + loadRow(props.rowId) +} + + useProvideSmartsheetStore(ref({}) as Ref, meta) provide(IsFormInj, ref(true)) diff --git a/packages/nc-gui/composables/useExpandedFormStore.ts b/packages/nc-gui/composables/useExpandedFormStore.ts index 2aec9d1bf6..84483102ff 100644 --- a/packages/nc-gui/composables/useExpandedFormStore.ts +++ b/packages/nc-gui/composables/useExpandedFormStore.ts @@ -175,13 +175,14 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m return data } - const loadRow = async () => { + const loadRow = async (rowId?: string) => { const record = await $api.dbTableRow.read( NOCO, (project?.value?.id || sharedView.value.view.project_id) as string, meta.value.title, - extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]), + rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]), ) + Object.assign(row.value, { row: record, oldRow: { ...record }, diff --git a/packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue b/packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]]/[[rowId]].vue similarity index 100% rename from packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue rename to packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]]/[[rowId]].vue From 5a6cb79714d0b2dea02e87d336c933dc29f67dd5 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Sep 2022 22:17:12 +0530 Subject: [PATCH 003/164] feat(gui): use query param for providing rowId Signed-off-by: Pranav C --- packages/nc-gui/components/smartsheet/Grid.vue | 17 ++++++++++------- .../smartsheet/expanded-form/Header.vue | 1 + .../smartsheet/expanded-form/index.vue | 4 +--- .../[[rowId]].vue => [[viewTitle]].vue} | 0 4 files changed, 12 insertions(+), 10 deletions(-) rename packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/{[[viewTitle]]/[[rowId]].vue => [[viewTitle]].vue} (100%) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 73546a870d..531529572f 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -139,8 +139,8 @@ const expandForm = (row: Row, state?: Record, fromToolbar = false) if (rowId) { router.push({ - params: { - ...route.params, + query: { + ...route.query, rowId, }, }) @@ -398,18 +398,21 @@ onBeforeUnmount(async () => { const expandedFormOnRowIdDlg = computed({ get() { - return !!route.params.rowId + return !!route.query.rowId }, set(val) { if (!val) router.push({ - params: { - ...route.params, + query: { + ...route.query, rowId: undefined, }, }) }, }) + +// reload table data reload hook as fallback to rowdatareload +provide(ReloadRowDataHookInj, reloadViewDataHook) diff --git a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue index a0d7621f39..7a56339e1d 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue @@ -26,6 +26,7 @@ const save = async () => { reloadTrigger?.trigger() } else { await _save() + reloadTrigger?.trigger() } } diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index 3e1ccaf8f2..45c450a275 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -53,16 +53,14 @@ provide(MetaInj, meta) const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row) - if (props.loadRow) { await loadRow() } if (props.rowId) { - loadRow(props.rowId) + await loadRow(props.rowId) } - useProvideSmartsheetStore(ref({}) as Ref, meta) provide(IsFormInj, ref(true)) diff --git a/packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]]/[[rowId]].vue b/packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue similarity index 100% rename from packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]]/[[rowId]].vue rename to packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue From d9e2133e61e729b087b96f79dedfb01212f228cb Mon Sep 17 00:00:00 2001 From: Pranav C Date: Fri, 16 Sep 2022 00:41:03 +0530 Subject: [PATCH 004/164] feat(gui): add copy record url option in expanded form Signed-off-by: Pranav C --- .../smartsheet/expanded-form/Header.vue | 21 ++++++++++++++++++- .../composables/useExpandedFormStore.ts | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue index 7a56339e1d..2a686ba147 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue @@ -1,4 +1,5 @@ + + + + -
+
From 004875e922e8b11302e35719ac329eee0ae863ef Mon Sep 17 00:00:00 2001 From: Pranav C Date: Fri, 16 Sep 2022 16:01:04 +0530 Subject: [PATCH 007/164] fix(gui): reload form view on clicking active form view Signed-off-by: Pranav C --- packages/nc-gui/components/smartsheet/expanded-form/Header.vue | 2 +- packages/nc-gui/components/tabs/Smartsheet.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue index cbe5314182..3b9c23098c 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue @@ -68,7 +68,7 @@ const copyRecordUrl = () => { - + From b8df66ec11308d99ddb17ffb0c592dde6e1e975b Mon Sep 17 00:00:00 2001 From: Pranav C Date: Fri, 16 Sep 2022 16:58:16 +0530 Subject: [PATCH 008/164] feat(gui): add row url feature in gallery view Signed-off-by: Pranav C --- .../nc-gui/components/smartsheet/Gallery.vue | 52 +++++++++++++++++-- .../nc-gui/components/smartsheet/Grid.vue | 2 - 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Gallery.vue b/packages/nc-gui/components/smartsheet/Gallery.vue index f946d8f779..e0dee6756c 100644 --- a/packages/nc-gui/components/smartsheet/Gallery.vue +++ b/packages/nc-gui/components/smartsheet/Gallery.vue @@ -18,6 +18,7 @@ import { } from '#imports' import Row from '~/components/smartsheet/Row.vue' import type { Row as RowType } from '~/composables' +import { extractPkFromRow } from '~/utils' import ImageIcon from '~icons/mdi/file-image-box' interface Attachment { @@ -54,6 +55,10 @@ provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable')) const fields = inject(FieldsInj, ref([])) +const route = useRoute() + +const router = useRouter() + const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== galleryData.value?.fk_cover_image_col_id)) const coverImageColumn: any = $( @@ -101,11 +106,23 @@ reloadViewDataHook?.on(async () => { }) }) -const expandForm = (row: RowType, state?: Record) => { +const expandForm = (row: RowType, _state?: Record) => { if (!isUIAllowed('xcDatatableEditable')) return - expandedFormRow.value = row - expandedFormRowState.value = state - expandedFormDlg.value = true + + const rowId = extractPkFromRow(row.row, meta.value.columns) + + if (rowId) { + router.push({ + query: { + ...route.query, + rowId, + }, + }) + } else { + expandedFormRow.value = row + expandedFormRowState.value = state + expandedFormDlg.value = true + } } const expandFormClick = async (e: MouseEvent, row: RowType) => { @@ -119,6 +136,24 @@ openNewRecordFormHook?.on(async () => { const newRow = await addEmptyRow() expandForm(newRow) }) + +const expandedFormOnRowIdDlg = computed({ + get() { + return !!route.query.rowId + }, + set(val) { + if (!val) + router.push({ + query: { + ...route.query, + rowId: undefined, + }, + }) + }, +}) + +// reload table data reload hook as fallback to rowdatareload +provide(ReloadRowDataHookInj, reloadViewDataHook) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 531529572f..1c35dda426 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -150,8 +150,6 @@ const expandForm = (row: Row, state?: Record, fromToolbar = false) expandedFormDlg.value = true skipRowRemovalOnCancel.value = !fromToolbar } - - /* */ } openNewRecordFormHook?.on(async () => { From c77653a0d8d9f6d80c0cd769dc398b7745e8f0ba Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Sep 2022 11:25:45 +0530 Subject: [PATCH 009/164] fix(api): if record not found response with 404 Signed-off-by: Pranav C --- .../nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts index 854262a5aa..b843af5d08 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts +++ b/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts @@ -3,6 +3,7 @@ import Model from '../../../models/Model'; import { nocoExecute } from 'nc-help'; import Base from '../../../models/Base'; import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; +import { NcError } from '../../helpers/catchError'; import { PagedResponseImpl } from '../../helpers/PagedResponse'; import View from '../../../models/View'; import ncMetaAclMw from '../../helpers/ncMetaAclMw'; @@ -92,6 +93,7 @@ async function dataDelete(req: Request, res: Response) { } res.json(await baseModel.delByPk(req.params.rowId, null, req)); } + async function getDataList(model, view: View, req) { const base = await Base.get(model.base_id); @@ -190,10 +192,16 @@ async function dataRead(req: Request, res: Response) { dbDriver: NcConnectionMgrv2.get(base), }); + const row = await baseModel.readByPk(req.params.rowId); + + if (!row) { + NcError.notFound(); + } + res.json( await nocoExecute( await getAst({ model, query: req.query, view }), - await baseModel.readByPk(req.params.rowId), + row, {}, {} ) @@ -213,6 +221,7 @@ async function dataExist(req: Request, res: Response) { res.json(await baseModel.exist(req.params.rowId)); } + const router = Router({ mergeParams: true }); // table data crud apis From 3c1476ffcf48a29ec6dca1969ec41841086e67fd Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Sep 2022 11:26:32 +0530 Subject: [PATCH 010/164] enhancement(gui): if record not found show error message and redirect to view Signed-off-by: Pranav C --- .../components/smartsheet/expanded-form/index.vue | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index 45c450a275..f9dd692eb0 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -1,4 +1,5 @@ diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index 4a7e4bf652..468ceb3c91 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -32,6 +32,7 @@ interface Props { loadRow?: boolean useMetaFields?: boolean rowId?: string + view?: ViewType } const props = defineProps() @@ -127,7 +128,7 @@ export default { :closable="false" class="nc-drawer-expanded-form" > -
+
From 240057b194dcd571e733d57dc62b6fba013ce07e Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Sep 2022 23:20:21 +0530 Subject: [PATCH 013/164] chore(cypress): toast wait command fix Signed-off-by: Pranav C --- scripts/cypress/support/commands.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/cypress/support/commands.js b/scripts/cypress/support/commands.js index ee7ba6bb35..0897d177fd 100644 --- a/scripts/cypress/support/commands.js +++ b/scripts/cypress/support/commands.js @@ -374,7 +374,8 @@ Cypress.Commands.add('renameTable', (oldName, newName) => { // }); Cypress.Commands.add('toastWait', (msg) => { - cy.get('.ant-message-notice-content:visible', { timeout: 60000 }).contains(msg).should('exist'); + // cy.get('.ant-message-notice-content:visible', { timout: 30000 }).should('exist') + cy.get(`.ant-message-notice-content:visible:contains("${msg}")`, { timeout: 30000 }).should('exist'); cy.get('.ant-message-notice-content:visible', { timeout: 12000 }).should('not.exist'); }); From 95039cd557c77548775740fda817debbd7fa04e1 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Sep 2022 23:21:55 +0530 Subject: [PATCH 014/164] wip(cypress): add tests for expanded form Signed-off-by: Pranav C --- .../smartsheet/expanded-form/Header.vue | 6 +- packages/nocodb-sdk/package-lock.json | 2 +- .../common/4g_table_view_expanded_form.js | 105 ++++++++++++++++++ .../cypress/integration/test/pg-restViews.js | 2 + scripts/cypress/integration/test/restViews.js | 2 + 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 scripts/cypress/integration/common/4g_table_view_expanded_form.js diff --git a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue index acd49bb6fe..86604edfaa 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue @@ -54,7 +54,7 @@ const copyRecordUrl = () => { - + @@ -97,7 +97,7 @@ const copyRecordUrl = () => { /> - + {{ $t('general.cancel') }} diff --git a/packages/nocodb-sdk/package-lock.json b/packages/nocodb-sdk/package-lock.json index b075fa7af6..6d5f785843 100644 --- a/packages/nocodb-sdk/package-lock.json +++ b/packages/nocodb-sdk/package-lock.json @@ -15806,4 +15806,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/scripts/cypress/integration/common/4g_table_view_expanded_form.js b/scripts/cypress/integration/common/4g_table_view_expanded_form.js new file mode 100644 index 0000000000..f09d0a17b9 --- /dev/null +++ b/scripts/cypress/integration/common/4g_table_view_expanded_form.js @@ -0,0 +1,105 @@ +import { isTestSuiteActive } from '../../support/page_objects/projectConstants'; + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +export const genTest = (apiType, dbType) => { + if (!isTestSuiteActive(apiType, dbType)) return; + + describe(`${apiType.toUpperCase()} api - Table views: Expanded form`, () => { + + before(() => { + cy.restoreLocalStorage(); + + // open a table to work on views + // + cy.openTableTab('Country', 25); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + after(() => { + cy.restoreLocalStorage(); + cy.closeTableTab('Country'); + cy.saveLocalStorage(); + }); + + // Common routine to create/edit/delete GRID & GALLERY view + // Input: viewType - 'grid'/'gallery' + // + const viewTest = (viewType) => { + it(`Create ${viewType} view`, () => { + // click on 'Grid/Gallery' button on Views bar + cy.get(`.nc-create-${viewType}-view`).click(); + + // Pop up window, click Submit (accepting default name for view) + cy.getActiveModal('.nc-modal-view-create').find('.ant-btn-primary').click(); + cy.toastWait('View created successfully'); + + // validate if view was created && contains default name 'Country1' + cy.get(`.nc-${viewType}-view-item`) + .contains(`${capitalizeFirstLetter(viewType)}-1`) + .should('exist'); + }); + + + it(`Expand a row in ${viewType} and verify url`, () => { + + if (viewType === 'grid') { + cy.get('.nc-row-expand') + .first() + .click({ force: true }); + } else if (viewType === 'gallery') { + cy.get('.nc-gallery-container .ant-card') + .first() + .click({ force: true }); + } + cy.url().should('include', 'rowId=1'); + + // copy url + cy.get('.nc-copy-row-url').click(); + + cy.window().then((win) => { + win.navigator.clipboard.readText().then((text) => { + expect(text).to.contains('?rowId=1'); + }); + }); + + cy.get('.nc-expand-form-close-btn').click(); + + }); + + it(`Visit a ${viewType} row url and verify expanded form`, () => { + cy.url() + .then((url) => { + cy.restoreLocalStorage(); + cy.visit('/' + url.split('/').slice(3).join('/').split('?')[0] + '?rowId=2'); + cy.get('.nc-expanded-form-header').should('exist'); + cy.saveLocalStorage(); + }); + }); + + it(`Visit an invalid ${viewType} row url and verify expanded form`, () => { + cy.url() + .then((url) => { + cy.restoreLocalStorage(); + cy.visit('/' + url.split('/').slice(3).join('/').split('?')[0] + '?rowId=99999999'); + cy.get('.nc-expanded-form-header').should('not.exist'); + cy.saveLocalStorage(); + }); + }); + + + }; + + viewTest('grid'); // grid view + viewTest('gallery'); // gallery view + }); +}; diff --git a/scripts/cypress/integration/test/pg-restViews.js b/scripts/cypress/integration/test/pg-restViews.js index 83c25820ca..0d3b1da94c 100644 --- a/scripts/cypress/integration/test/pg-restViews.js +++ b/scripts/cypress/integration/test/pg-restViews.js @@ -6,6 +6,7 @@ let t4c = require("../common/4c_form_view_detailed"); let t4d = require("../common/4d_table_view_grid_locked"); let t4e = require("../common/4e_form_view_share"); let t4f = require("../common/4f_pg_grid_view_share"); +let t4g = require("../common/4g_table_view_expanded_form"); const { setCurrentMode, } = require("../../support/page_objects/projectConstants"); @@ -20,6 +21,7 @@ const nocoTestSuite = (apiType, dbType) => { t4b.genTest(apiType, dbType); t4d.genTest(apiType, dbType); t4e.genTest(apiType, dbType); + t4g.genTest(apiType, dbType); // tbd t4f.genTest(apiType, dbType); }; diff --git a/scripts/cypress/integration/test/restViews.js b/scripts/cypress/integration/test/restViews.js index a96ea487a1..5f4488e7f6 100644 --- a/scripts/cypress/integration/test/restViews.js +++ b/scripts/cypress/integration/test/restViews.js @@ -6,6 +6,7 @@ let t4c = require("../common/4c_form_view_detailed"); let t4d = require("../common/4d_table_view_grid_locked"); let t4e = require("../common/4e_form_view_share"); let t4f = require("../common/4f_grid_view_share"); +let t4g = require("../common/4g_table_view_expanded_form"); const { setCurrentMode, } = require("../../support/page_objects/projectConstants"); @@ -21,6 +22,7 @@ const nocoTestSuite = (apiType, dbType) => { t4d.genTest(apiType, dbType); t4e.genTest(apiType, dbType); t4f.genTest(apiType, dbType); + t4g.genTest(apiType, dbType); }; nocoTestSuite("rest", "mysql"); From 06f9c092074e1f751393d40adade0bc4e76f65a4 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 19 Sep 2022 12:42:29 +0530 Subject: [PATCH 015/164] chore(cypress): clipboard verification correction Signed-off-by: Pranav C --- .../common/4g_table_view_expanded_form.js | 15 +++++------ scripts/cypress/support/commands.js | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/scripts/cypress/integration/common/4g_table_view_expanded_form.js b/scripts/cypress/integration/common/4g_table_view_expanded_form.js index f09d0a17b9..a3a23514dc 100644 --- a/scripts/cypress/integration/common/4g_table_view_expanded_form.js +++ b/scripts/cypress/integration/common/4g_table_view_expanded_form.js @@ -63,14 +63,15 @@ export const genTest = (apiType, dbType) => { } cy.url().should('include', 'rowId=1'); + // spy on clipboard to verify copied text + cy.window().then((win) => { + cy.spy(win.navigator.clipboard, 'writeText').as('copy'); + }) + // copy url cy.get('.nc-copy-row-url').click(); - cy.window().then((win) => { - win.navigator.clipboard.readText().then((text) => { - expect(text).to.contains('?rowId=1'); - }); - }); + cy.get('@copy').should('be.calledWithMatch', `?rowId=1`); cy.get('.nc-expand-form-close-btn').click(); @@ -79,20 +80,16 @@ export const genTest = (apiType, dbType) => { it(`Visit a ${viewType} row url and verify expanded form`, () => { cy.url() .then((url) => { - cy.restoreLocalStorage(); cy.visit('/' + url.split('/').slice(3).join('/').split('?')[0] + '?rowId=2'); cy.get('.nc-expanded-form-header').should('exist'); - cy.saveLocalStorage(); }); }); it(`Visit an invalid ${viewType} row url and verify expanded form`, () => { cy.url() .then((url) => { - cy.restoreLocalStorage(); cy.visit('/' + url.split('/').slice(3).join('/').split('?')[0] + '?rowId=99999999'); cy.get('.nc-expanded-form-header').should('not.exist'); - cy.saveLocalStorage(); }); }); diff --git a/scripts/cypress/support/commands.js b/scripts/cypress/support/commands.js index 0897d177fd..35a6cdb2af 100644 --- a/scripts/cypress/support/commands.js +++ b/scripts/cypress/support/commands.js @@ -141,7 +141,7 @@ Cypress.Commands.add('refreshTableTab', () => { .first() .rightclick({ force: true }); - cy.getActiveMenu(".nc-dropdown-tree-view-context-menu") + cy.getActiveMenu('.nc-dropdown-tree-view-context-menu') .find('[role="menuitem"]') .contains('Tables Refresh') .should('exist') @@ -242,13 +242,16 @@ Cypress.Commands.add('saveLocalStorage', (name) => { cy.printLocalStorage(); }); -Cypress.Commands.add('restoreLocalStorage', (name) => { - Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => { - localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]); - }); +const restoreLocalStorage = () => { + Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => { + localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]); + }); - cy.printLocalStorage(); -}); + cy.printLocalStorage(); +}; + +Cypress.Commands.add('restoreLocalStorage', restoreLocalStorage); +Cypress.on('window:before:load', restoreLocalStorage); Cypress.Commands.add('deleteLocalStorage', () => { Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => { @@ -322,7 +325,7 @@ Cypress.Commands.add('createTable', (name) => { Cypress.Commands.add('deleteTable', (name, dbType) => { cy.get(`.nc-project-tree-tbl-${name}`).should('exist').rightclick(); - cy.getActiveMenu(".nc-dropdown-tree-view-context-menu").find('[role="menuitem"]').contains('Delete').click(); + cy.getActiveMenu('.nc-dropdown-tree-view-context-menu').find('[role="menuitem"]').contains('Delete').click(); cy.getActiveModal().find('button').contains('Yes').click(); cy.toastWait(`Deleted table successfully`); @@ -337,16 +340,16 @@ Cypress.Commands.add('renameTable', (oldName, newName) => { .rightclick(); // choose rename option from menu - cy.getActiveMenu(".nc-dropdown-tree-view-context-menu") + cy.getActiveMenu('.nc-dropdown-tree-view-context-menu') .find('[role="menuitem"]') .contains('Rename') .click({ force: true }); // feed new name - cy.getActiveModal(".nc-modal-table-rename").find('input').clear().type(newName); + cy.getActiveModal('.nc-modal-table-rename').find('input').clear().type(newName); // submit - cy.getActiveModal(".nc-modal-table-rename").find('button').contains('Submit').click(); + cy.getActiveModal('.nc-modal-table-rename').find('button').contains('Submit').click(); cy.toastWait('Table renamed successfully'); @@ -475,7 +478,7 @@ Cypress.Commands.add('signOut', () => { cy.visit(`/`); cy.get('.nc-project-page-title', { timeout: 30000 }).contains('My Projects').should('be.visible'); cy.get('.nc-menu-accounts', { timeout: 30000 }).should('exist').click(); - cy.getActiveMenu(".nc-dropdown-user-accounts-menu").find('.ant-dropdown-menu-item').eq(1).click(); + cy.getActiveMenu('.nc-dropdown-user-accounts-menu').find('.ant-dropdown-menu-item').eq(1).click(); cy.wait(5000); cy.get('button:contains("SIGN")').should('exist'); From 8434d8ccb427c244b71cb72d5e31a823da7e55ea Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 19 Sep 2022 13:05:14 +0530 Subject: [PATCH 016/164] chore(cypress): verify invalid url by checking toast and expanded form Signed-off-by: Pranav C --- packages/nc-gui/components/smartsheet/Gallery.vue | 2 +- .../cypress/integration/common/4g_table_view_expanded_form.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/components/smartsheet/Gallery.vue b/packages/nc-gui/components/smartsheet/Gallery.vue index 1ef419e23b..3018481b80 100644 --- a/packages/nc-gui/components/smartsheet/Gallery.vue +++ b/packages/nc-gui/components/smartsheet/Gallery.vue @@ -157,7 +157,7 @@ provide(ReloadRowDataHookInj, reloadViewDataHook)