Browse Source

Nc feat/auto generate field name based on field type (#8644)

* feat(nc-gui): auto generate new field name based on field type

* feat(nc-gui): MFE - auto generate new field name

* fix(nc-gui): update MFE field name input placeholder style

* fix(nc-gui): auto generate field name in MFE title upadate issue

* fix(nc-gui): remove plural table name if it is oo link type

* fix(nc-gui): pr review changes

* fix(nc-gui): MFE field name input style

* chore(nc-gui): lint

* chore(nc-gui): lint

* fix(nc-gui): update lookup & rollup column tooltip

* fix(nc-gui): multi field editor test fail issue

* refactor(nc-gui): multifield editor auto generate column name code

* test: update survey form verify submit msg test

* chore(nc-gui): lint
pull/8671/head
Ramesh Mane 6 months ago committed by GitHub
parent
commit
3cc4a38469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  2. 5
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  3. 15
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  4. 41
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  5. 50
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  6. 2
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  7. 5
      packages/nc-gui/components/smartsheet/column/UserOptions.vue
  8. 63
      packages/nc-gui/components/smartsheet/details/Fields.vue
  9. 24
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  10. 70
      packages/nc-gui/composables/useColumnCreateStore.ts
  11. 208
      packages/nc-gui/helpers/parsers/parserHelpers.ts
  12. 44
      packages/nocodb-sdk/src/lib/UITypes.ts
  13. 1
      packages/nocodb-sdk/src/lib/index.ts
  14. 10
      tests/playwright/pages/Dashboard/SurveyForm/index.ts
  15. 3
      tests/playwright/tests/db/views/viewForm.spec.ts
  16. 1
      tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

10
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -325,8 +325,10 @@ const filterOption = (input: string, option: { value: UITypes }) => {
ref="antInput" ref="antInput"
v-model="formState.title" v-model="formState.title"
:disabled="readOnly" :disabled="readOnly"
class="flex flex-grow nc-fields-input text-lg font-bold outline-none bg-inherit" :placeholder="`${$t('objects.field')} ${$t('general.name').toLowerCase()} ${isEdit ? '' : $t('labels.optional')}`"
class="flex flex-grow nc-fields-input text-sm font-semibold outline-none bg-inherit min-h-6"
:contenteditable="true" :contenteditable="true"
@input="formState.userHasChangedTitle = true"
/> />
</div> </div>
</a-form-item> </a-form-item>
@ -335,6 +337,7 @@ const filterOption = (input: string, option: { value: UITypes }) => {
ref="antInput" ref="antInput"
v-model:value="formState.title" v-model:value="formState.title"
class="nc-column-name-input !rounded-lg" class="nc-column-name-input !rounded-lg"
:placeholder="`${$t('objects.field')} ${$t('general.name').toLowerCase()} ${isEdit ? '' : $t('labels.optional')}`"
:disabled="isKanban || readOnly" :disabled="isKanban || readOnly"
@input="onAlter(8)" @input="onAlter(8)"
/> />
@ -549,6 +552,11 @@ const filterOption = (input: string, option: { value: UITypes }) => {
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.nc-fields-input {
&::placeholder {
@apply font-normal;
}
}
.nc-column-name-input, .nc-column-name-input,
:deep(.nc-formula-input), :deep(.nc-formula-input),
:deep(.ant-form-item-control-input-content > input.ant-input) { :deep(.ant-form-item-control-input-content > input.ant-input) {

5
packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -8,6 +8,7 @@ interface Props {
preload?: Partial<ColumnType> preload?: Partial<ColumnType>
tableExplorerColumns?: ColumnType[] tableExplorerColumns?: ColumnType[]
fromTableExplorer?: boolean fromTableExplorer?: boolean
isColumnValid?: (value: Partial<ColumnType>) => boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -16,9 +17,9 @@ const emit = defineEmits(['submit', 'cancel', 'mounted'])
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const { column, preload, tableExplorerColumns, fromTableExplorer } = toRefs(props) const { column, preload, tableExplorerColumns, fromTableExplorer, isColumnValid } = toRefs(props)
useProvideColumnCreateStore(meta, column, tableExplorerColumns, fromTableExplorer) useProvideColumnCreateStore(meta, column, tableExplorerColumns, fromTableExplorer, isColumnValid)
</script> </script>
<template> <template>

15
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -16,7 +16,7 @@ const meta = inject(MetaInj, ref())
const filterRef = ref() const filterRef = ref()
const { setAdditionalValidations, setPostSaveOrUpdateCbk, validateInfos, onDataTypeChange, sqlUi, isXcdbBase } = const { setAdditionalValidations, setPostSaveOrUpdateCbk, validateInfos, onDataTypeChange, sqlUi, isXcdbBase, updateFieldName } =
useColumnCreateStoreOrThrow() useColumnCreateStoreOrThrow()
const baseStore = useBase() const baseStore = useBase()
@ -134,6 +134,7 @@ const referenceTableChildId = computed({
set: (value) => { set: (value) => {
if (!isEdit.value && value) { if (!isEdit.value && value) {
vModel.value.childId = value vModel.value.childId = value
vModel.value.childTableTitle = refTables.value.find((t) => t.id === value)?.title
} }
}, },
}) })
@ -143,9 +144,19 @@ const linkType = computed({
set: (value) => { set: (value) => {
if (!isEdit.value && value) { if (!isEdit.value && value) {
vModel.value.type = value vModel.value.type = value
updateFieldName()
} }
}, },
}) })
const handleUpdateRefTable = () => {
onDataTypeChange()
nextTick(() => {
updateFieldName()
})
}
</script> </script>
<template> <template>
@ -182,7 +193,7 @@ const linkType = computed({
:filter-option="filterOption" :filter-option="filterOption"
placeholder="select table to link" placeholder="select table to link"
dropdown-class-name="nc-dropdown-ltar-child-table" dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange" @change="handleUpdateRefTable"
> >
<template #suffixIcon> <template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" /> <GeneralIcon icon="arrowDown" class="text-gray-700" />

41
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, LookupType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
@ -15,7 +15,8 @@ const meta = inject(MetaInj, ref())
const { t } = useI18n() const { t } = useI18n()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit, disableSubmitBtn } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit, disableSubmitBtn, updateFieldName } =
useColumnCreateStoreOrThrow()
const baseStore = useBase() const baseStore = useBase()
@ -68,11 +69,19 @@ onMounted(() => {
} }
}) })
const getNextColumnId = () => {
const usedLookupColumnIds = (meta.value?.columns || [])
.filter((c) => c.uidt === UITypes.Lookup)
.map((c) => (c.colOptions as LookupType)?.fk_lookup_column_id)
return columns.value.find((c) => !usedLookupColumnIds.includes(c.id))?.id
}
const onRelationColChange = async () => { const onRelationColChange = async () => {
if (selectedTable.value) { if (selectedTable.value) {
await getMeta(selectedTable.value.id) await getMeta(selectedTable.value.id)
} }
vModel.value.fk_lookup_column_id = columns.value?.[0]?.id vModel.value.fk_lookup_column_id = getNextColumnId() || columns.value?.[0]?.id
onDataTypeChange() onDataTypeChange()
} }
@ -88,6 +97,32 @@ const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), { h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column, columnMeta: column,
}) })
watch(
() => vModel.value.fk_relation_column_id,
(newValue) => {
if (!newValue) return
const selectedTable = refTables.value.find((t) => t.col.fk_column_id === newValue)
if (selectedTable) {
vModel.value.lookupTableTitle = selectedTable?.title || selectedTable.table_name
}
},
)
watch(
() => vModel.value.fk_lookup_column_id,
(newValue) => {
if (!newValue) return
const selectedColumn = columns.value.find((c) => c.id === newValue)
if (selectedColumn) {
vModel.value.lookupColumnTitle = selectedColumn?.title || selectedColumn.column_name
updateFieldName()
}
},
)
</script> </script>
<template> <template>

50
packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -1,6 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import { type ColumnType, type LinkToAnotherRecordType, RelationTypes, type TableType, type UITypes } from 'nocodb-sdk' import {
type ColumnType,
type LinkToAnotherRecordType,
RelationTypes,
type RollupType,
type TableType,
UITypes,
} from 'nocodb-sdk'
import { getAvailableRollupForUiType, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { getAvailableRollupForUiType, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
@ -13,7 +20,8 @@ const vModel = useVModel(props, 'value', emit)
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit, disableSubmitBtn } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit, disableSubmitBtn, updateFieldName } =
useColumnCreateStoreOrThrow()
const baseStore = useBase() const baseStore = useBase()
@ -79,11 +87,19 @@ onMounted(() => {
} }
}) })
const getNextColumnId = () => {
const usedLookupColumnIds = (meta.value?.columns || [])
.filter((c) => c.uidt === UITypes.Rollup)
.map((c) => (c.colOptions as RollupType)?.fk_rollup_column_id)
return columns.value.find((c) => !usedLookupColumnIds.includes(c.id))?.id
}
const onRelationColChange = async () => { const onRelationColChange = async () => {
if (selectedTable.value) { if (selectedTable.value) {
await getMeta(selectedTable.value.id) await getMeta(selectedTable.value.id)
} }
vModel.value.fk_rollup_column_id = columns.value?.[0]?.id vModel.value.fk_rollup_column_id = getNextColumnId() || columns.value?.[0]?.id
onDataTypeChange() onDataTypeChange()
} }
@ -120,6 +136,15 @@ const filteredColumns = computed(() => {
}) })
}) })
const onRollupFunctionChange = () => {
const rollupFun = aggFunctionsList.value.find((func) => func.value === vModel.value.rollup_function)
if (rollupFun && rollupFun?.text) {
vModel.value.rollup_function_name = rollupFun.text
}
onDataTypeChange()
updateFieldName()
}
watch( watch(
() => vModel.value.fk_rollup_column_id, () => vModel.value.fk_rollup_column_id,
() => { () => {
@ -131,7 +156,12 @@ watch(
// when the previous roll up function was numeric type and the current child field is non-numeric // when the previous roll up function was numeric type and the current child field is non-numeric
// reset rollup function with a non-numeric type // reset rollup function with a non-numeric type
vModel.value.rollup_function = aggFunctionsList.value[0].value vModel.value.rollup_function = aggFunctionsList.value[0].value
vModel.value.rollup_function_name = aggFunctionsList.value[0].text
} }
vModel.value.rollupColumnTitle = childFieldColumn?.title || childFieldColumn?.column_name
updateFieldName()
}, },
) )
@ -142,6 +172,18 @@ watchEffect(() => {
disableSubmitBtn.value = false disableSubmitBtn.value = false
} }
}) })
watch(
() => vModel.value.fk_relation_column_id,
(newValue) => {
if (!newValue) return
const selectedTable = refTables.value.find((t) => t.col.fk_column_id === newValue)
if (selectedTable) {
vModel.value.rollupTableTitle = selectedTable?.title || selectedTable.table_name
}
},
)
</script> </script>
<template> <template>
@ -235,7 +277,7 @@ watchEffect(() => {
placeholder="-select-" placeholder="-select-"
dropdown-class-name="nc-dropdown-rollup-function" dropdown-class-name="nc-dropdown-rollup-function"
class="!mt-0.5" class="!mt-0.5"
@change="onDataTypeChange" @change="onRollupFunctionChange"
> >
<template #suffixIcon> <template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" /> <GeneralIcon icon="arrowDown" class="text-gray-700" />

2
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -22,7 +22,7 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, isEdit } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
// const { base } = storeToRefs(useBase()) // const { base } = storeToRefs(useBase())

5
packages/nc-gui/components/smartsheet/column/UserOptions.vue

@ -14,7 +14,7 @@ const initialIsMulti = ref()
const validators = {} const validators = {}
const { setAdditionalValidations } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, updateFieldName } = useColumnCreateStoreOrThrow()
setAdditionalValidations({ setAdditionalValidations({
...validators, ...validators,
@ -36,6 +36,7 @@ const updateIsMulti = (isChecked: boolean) => {
if (!vModel.value.meta.is_multi) { if (!vModel.value.meta.is_multi) {
vModel.value.cdf = vModel.value.cdf?.split(',')[0] || null vModel.value.cdf = vModel.value.cdf?.split(',')[0] || null
} }
updateFieldName()
} }
</script> </script>
@ -50,7 +51,7 @@ const updateIsMulti = (isChecked: boolean) => {
</a-form-item> </a-form-item>
<a-form-item v-if="future"> <a-form-item v-if="future">
<div v-if="vModel.meta" class="flex items-center gap-1"> <div v-if="vModel.meta" class="flex items-center gap-1">
<NcSwitch v-model:checked="vModel.meta.notify" data-testid="user-column-notify-user" @change="updateIsMulti"> <NcSwitch v-model:checked="vModel.meta.notify" data-testid="user-column-notify-user">
<div class="text-sm text-gray-800 select-none">Notify users with base access when they're added</div> <div class="text-sm text-gray-800 select-none">Notify users with base access when they're added</div>
</NcSwitch> </NcSwitch>
</div> </div>

63
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -5,6 +5,7 @@ import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk
import type { ColumnType, FilterType, SelectOptionsType } from 'nocodb-sdk' import type { ColumnType, FilterType, SelectOptionsType } from 'nocodb-sdk'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { onKeyDown, useMagicKeys } from '@vueuse/core' import { onKeyDown, useMagicKeys } from '@vueuse/core'
import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers'
interface TableExplorerColumn extends ColumnType { interface TableExplorerColumn extends ColumnType {
id?: string id?: string
@ -14,6 +15,7 @@ interface TableExplorerColumn extends ColumnType {
view_id: string view_id: string
} }
view_id?: string view_id?: string
userHasChangedTitle?: boolean
} }
interface op { interface op {
@ -347,6 +349,15 @@ const onFieldUpdate = (state: TableExplorerColumn, skipLinkChecks = false) => {
column: state, column: state,
}) })
} }
if (
activeField.value &&
Object.keys(activeField.value).length &&
((state?.id && activeField.value?.id && state?.id === activeField.value?.id) ||
(state?.temp_id && activeField.value?.temp_id && state?.temp_id === activeField.value?.temp_id))
) {
activeField.value = state
}
} }
} }
@ -467,7 +478,7 @@ const isColumnValid = (column: TableExplorerColumn) => {
const isDeleteOp = ops.value.find((op) => compareCols(column, op.column) && op.op === 'delete') const isDeleteOp = ops.value.find((op) => compareCols(column, op.column) && op.op === 'delete')
const isNew = ops.value.find((op) => compareCols(column, op.column) && op.op === 'add') const isNew = ops.value.find((op) => compareCols(column, op.column) && op.op === 'add')
if (isDeleteOp) return true if (isDeleteOp) return true
if (!column.title) { if (!column.title && !isNew) {
return false return false
} }
if ((column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord) && isNew) { if ((column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord) && isNew) {
@ -604,10 +615,23 @@ const saveChanges = async () => {
if (!meta.value?.id) return if (!meta.value?.id) return
loading.value = true loading.value = true
const newFieldTitles: string[] = []
for (const mop of moveOps.value) { for (const mop of moveOps.value) {
const op = ops.value.find((op) => compareCols(op.column, mop.column)) const op = ops.value.find((op) => compareCols(op.column, mop.column))
if (op && op.op === 'add') { if (op && op.op === 'add') {
if (!op.column?.userHasChangedTitle && !op.column.title) {
const defaultColumnName = generateUniqueColumnName({
formState: op.column,
tableExplorerColumns: fields.value || [],
metaColumns: meta.value?.columns || [],
newFieldTitles,
})
newFieldTitles.push(defaultColumnName)
op.column.title = defaultColumnName
op.column.column_name = defaultColumnName
}
op.column.column_order = { op.column.column_order = {
order: mop.order, order: mop.order,
view_id: view.value?.id as string, view_id: view.value?.id as string,
@ -826,13 +850,37 @@ const onFieldOptionUpdate = () => {
} }
watch( watch(
fields, () => activeField.value?.temp_id,
() => { (_newValue, oldValue) => {
if (activeField.value) { if (!oldValue) return
activeField.value = fields.value.find((field) => field.id === activeField.value.id) || activeField.value
const oldField = fields.value.find((field) => field.temp_id === oldValue)
if (
!oldField ||
(oldField &&
(oldField.title ||
!ops.value.find((op) => op.op === 'add' && op.column.temp_id === oldField.temp_id) ||
oldField?.userHasChangedTitle ||
!isColumnValid(oldField)))
) {
return
} }
const newFieldTitles = ops.value
.filter((op) => op.op === 'add' && op.column.title)
.map((op) => op.column.title)
.filter((t) => t) as string[]
const defaultColumnName = generateUniqueColumnName({
formState: oldField,
tableExplorerColumns: fields.value || [],
metaColumns: meta.value?.columns || [],
newFieldTitles,
})
oldField.title = defaultColumnName
oldField.column_name = defaultColumnName
}, },
{ deep: true },
) )
</script> </script>
@ -1279,6 +1327,7 @@ watch(
:column="activeField" :column="activeField"
:preload="fieldState(activeField)" :preload="fieldState(activeField)"
:table-explorer-columns="fields" :table-explorer-columns="fields"
:is-column-valid="isColumnValid"
embed-mode embed-mode
:readonly="isLocked" :readonly="isLocked"
from-table-explorer from-table-explorer

24
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -48,8 +48,6 @@ const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(false)) const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(false))
const colOptions = computed(() => column.value?.colOptions)
const tableTile = computed(() => meta?.value?.title) const tableTile = computed(() => meta?.value?.title)
const relationColumnOptions = computed<LinkToAnotherRecordType | null>(() => { const relationColumnOptions = computed<LinkToAnotherRecordType | null>(() => {
@ -70,22 +68,6 @@ const relatedTableMeta = computed(
const relatedTableTitle = computed(() => relatedTableMeta.value?.title) const relatedTableTitle = computed(() => relatedTableMeta.value?.title)
const childColumn = computed(() => {
if (relatedTableMeta.value?.columns) {
if (isRollup(column.value)) {
return relatedTableMeta.value?.columns.find(
(c: ColumnType) => c.id === (colOptions.value as RollupType).fk_rollup_column_id,
)
}
if (isLookup(column.value)) {
return relatedTableMeta.value?.columns.find(
(c: ColumnType) => c.id === (colOptions.value as LookupType).fk_lookup_column_id,
)
}
}
return ''
})
const tooltipMsg = computed(() => { const tooltipMsg = computed(() => {
if (!column.value) { if (!column.value) {
return '' return ''
@ -99,8 +81,6 @@ const tooltipMsg = computed(() => {
return `'${column?.value?.title}' ${t('labels.belongsTo')} '${relatedTableTitle.value}'` return `'${column?.value?.title}' ${t('labels.belongsTo')} '${relatedTableTitle.value}'`
} else if (isOo(column.value)) { } else if (isOo(column.value)) {
return `'${tableTile.value}' & '${relatedTableTitle.value}' ${t('labels.oneToOne')}` return `'${tableTile.value}' & '${relatedTableTitle.value}' ${t('labels.oneToOne')}`
} else if (isLookup(column.value)) {
return `'${childColumn.value.title}' from '${relatedTableTitle.value}' (${childColumn.value.uidt})`
} else if (isFormula(column.value)) { } else if (isFormula(column.value)) {
const formula = substituteColumnIdWithAliasInFormula( const formula = substituteColumnIdWithAliasInFormula(
(column.value?.colOptions as FormulaType)?.formula, (column.value?.colOptions as FormulaType)?.formula,
@ -108,14 +88,12 @@ const tooltipMsg = computed(() => {
(column.value?.colOptions as any)?.formula_raw, (column.value?.colOptions as any)?.formula_raw,
) )
return `Formula - ${formula}` return `Formula - ${formula}`
} else if (isRollup(column.value)) {
return `'${childColumn.value.title}' of '${relatedTableTitle.value}' (${childColumn.value.uidt})`
} }
return column?.value?.title || '' return column?.value?.title || ''
}) })
const showTooltipAlways = computed(() => { const showTooltipAlways = computed(() => {
return isLinksOrLTAR(column.value) || isFormula(column.value) || isRollup(column.value) || isLookup(column.value) return isLinksOrLTAR(column.value) || isFormula(column.value)
}) })
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null) const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)

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

@ -3,6 +3,7 @@ import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form' import type { RuleObject } from 'ant-design-vue/es/form'
import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers'
const clone = rfdc() const clone = rfdc()
@ -20,6 +21,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
column: Ref<ColumnType | undefined>, column: Ref<ColumnType | undefined>,
tableExplorerColumns?: Ref<ColumnType[] | undefined>, tableExplorerColumns?: Ref<ColumnType[] | undefined>,
fromTableExplorer?: Ref<boolean | undefined>, fromTableExplorer?: Ref<boolean | undefined>,
isColumnValid?: Ref<((value: Partial<ColumnType>) => boolean) | undefined>,
) => { ) => {
const baseStore = useBase() const baseStore = useBase()
@ -69,33 +71,21 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
} }
const formState = ref<Record<string, any>>({ const formState = ref<Record<string, any>>({
title: 'title', title: '',
uidt: fromTableExplorer?.value ? UITypes.SingleLineText : null, uidt: fromTableExplorer?.value ? UITypes.SingleLineText : null,
...clone(column.value || {}), ...clone(column.value || {}),
}) })
const generateUniqueColumnSuffix = () => {
let suffix = (meta.value?.columns?.length || 0) + 1
let columnName = `title${suffix}`
while (
(tableExplorerColumns?.value || meta.value?.columns)?.some(
(c) => (c.column_name || '').toLowerCase() === columnName.toLowerCase(),
)
) {
suffix++
columnName = `title${suffix}`
}
return suffix
}
// actions // actions
const generateNewColumnMeta = (ignoreUidt = false) => { const generateNewColumnMeta = (ignoreUidt = false) => {
setAdditionalValidations({}) setAdditionalValidations({})
formState.value = { formState.value = {
meta: {}, meta: {},
...sqlUi.value.getNewColumn(generateUniqueColumnSuffix()), ...sqlUi.value.getNewColumn(1),
} }
formState.value.title = formState.value.column_name formState.value.title = ''
formState.value.column_name = ''
if (ignoreUidt && !fromTableExplorer?.value) { if (ignoreUidt && !fromTableExplorer?.value) {
formState.value.uidt = null formState.value.uidt = null
} }
@ -104,10 +94,14 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const validators = computed(() => { const validators = computed(() => {
return { return {
title: [ title: [
...(isEdit.value
? [
{ {
required: true, required: true,
message: t('msg.error.columnNameRequired'), message: t('msg.error.columnNameRequired'),
}, },
]
: []),
// validation for unique column name // validation for unique column name
{ {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
@ -158,16 +152,19 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const onUidtOrIdTypeChange = () => { const onUidtOrIdTypeChange = () => {
disableSubmitBtn.value = false disableSubmitBtn.value = false
const newTitle = updateFieldName(false)
const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined) const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined)
formState.value = { formState.value = {
...(!isEdit.value && { ...(!isEdit.value && {
// only take title, column_name and uidt when creating a column // only take title, column_name and uidt when creating a column
// to avoid the extra props from being taken (e.g. SingleLineText -> LTAR -> SingleLineText) // to avoid the extra props from being taken (e.g. SingleLineText -> LTAR -> SingleLineText)
// to mess up the column creation // to mess up the column creation
title: formState.value.title, title: newTitle || formState.value.title,
column_name: formState.value.column_name, column_name: newTitle || formState.value.column_name,
uidt: formState.value.uidt, uidt: formState.value.uidt,
temp_id: formState.value.temp_id, temp_id: formState.value.temp_id,
userHasChangedTitle: !!formState.value?.userHasChangedTitle,
}), }),
...(isEdit.value && { ...(isEdit.value && {
// take the existing formState.value when editing a column // take the existing formState.value when editing a column
@ -285,6 +282,16 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
// Column updated // Column updated
// message.success(t('msg.success.columnUpdated')) // message.success(t('msg.success.columnUpdated'))
} else { } else {
// set default field title
if (!formState.value.title.trim()) {
const columnName = generateUniqueColumnName({
formState: formState.value,
tableExplorerColumns: tableExplorerColumns?.value,
metaColumns: meta.value?.columns || [],
})
formState.value.title = columnName
formState.value.column_name = columnName
}
// todo : set additional meta for auto generated string id // todo : set additional meta for auto generated string id
if (formState.value.uidt === UITypes.ID) { if (formState.value.uidt === UITypes.ID) {
// based on id column type set autogenerated meta prop // based on id column type set autogenerated meta prop
@ -323,6 +330,30 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
} }
} }
function updateFieldName(updateFormState: boolean = true) {
if (
isEdit.value ||
!fromTableExplorer?.value ||
formState.value?.userHasChangedTitle ||
!isColumnValid?.value?.(formState.value)
) {
return
}
const defaultColumnName = generateUniqueColumnName({
formState: formState.value,
tableExplorerColumns: tableExplorerColumns?.value || [],
metaColumns: meta.value?.columns || [],
})
if (updateFormState) {
formState.value.title = defaultColumnName
formState.value.column_name = defaultColumnName
} else {
return defaultColumnName
}
}
/** set column name same as title which is actual name in db */ /** set column name same as title which is actual name in db */
watch( watch(
() => formState.value?.title, () => formState.value?.title,
@ -349,6 +380,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBase, isXcdbBase,
disableSubmitBtn, disableSubmitBtn,
setPostSaveOrUpdateCbk, setPostSaveOrUpdateCbk,
updateFieldName,
} }
}, },
) )

208
packages/nc-gui/helpers/parsers/parserHelpers.ts

@ -1,5 +1,6 @@
import { UITypes } from 'nocodb-sdk' import { type ColumnType, FieldNameFromUITypes, UITypes } from 'nocodb-sdk'
import isURL from 'validator/lib/isURL' import isURL from 'validator/lib/isURL'
import { pluralize } from 'inflection'
// This regex pattern matches email addresses by looking for sequences that start with characters before the "@" symbol, followed by the domain. // This regex pattern matches email addresses by looking for sequences that start with characters before the "@" symbol, followed by the domain.
// It's designed to capture most email formats, including those with periods and "+" symbols in the local part. // It's designed to capture most email formats, including those with periods and "+" symbols in the local part.
@ -279,3 +280,208 @@ export const getFormattedViewTabTitle = ({
return title return title
} }
export const generateUniqueColumnSuffix = ({
tableExplorerColumns,
metaColumns,
}: {
tableExplorerColumns?: ColumnType[]
metaColumns: ColumnType[]
}) => {
let suffix = (metaColumns?.length || 0) + 1
let columnName = `title${suffix}`
while (
(tableExplorerColumns || metaColumns)?.some(
(c) =>
(c.column_name || '').toLowerCase() === columnName.toLowerCase() ||
(c.title || '').toLowerCase() === columnName.toLowerCase(),
)
) {
suffix++
columnName = `title${suffix}`
}
return suffix
}
const extractNextDefaultColumnName = ({
tableExplorerColumns,
metaColumns,
defaultColumnName,
newFieldTitles,
formState,
}: {
tableExplorerColumns?: ColumnType[]
metaColumns: ColumnType[]
defaultColumnName: string
newFieldTitles: string[]
formState: Record<string, any>
}): string => {
// Extract and sort numbers associated with the provided defaultName
const namesData = ((tableExplorerColumns || metaColumns)
?.flatMap((c) => {
if (formState?.temp_id && c?.temp_id && formState?.temp_id === c?.temp_id) {
return []
}
if (c.title !== c.column_name) {
return [c.title?.toLowerCase(), c.column_name?.toLowerCase()]
}
return [c.title?.toLowerCase()]
})
.filter((t) => t && t.startsWith(defaultColumnName.toLowerCase())) || []) as string[]
if (![...namesData, ...newFieldTitles].includes(defaultColumnName.toLowerCase())) {
return defaultColumnName
}
const extractedSortedNumbers =
(namesData
.map((name) => {
const [_defaultName, number] = name.split(/ (?!.* )/)
if (_defaultName === defaultColumnName.toLowerCase() && !isNaN(Number(number?.trim()))) {
return Number(number?.trim())
}
return undefined
})
.filter((e) => e)
.sort((a, b) => {
if (a !== undefined && b !== undefined) {
return a - b
}
return 0
}) as number[]) || []
return extractedSortedNumbers.length
? `${defaultColumnName} ${extractedSortedNumbers[extractedSortedNumbers.length - 1] + 1}`
: `${defaultColumnName} 1`
}
export const generateUniqueColumnName = ({
tableExplorerColumns,
metaColumns,
formState,
newFieldTitles,
}: {
tableExplorerColumns?: ColumnType[]
metaColumns: ColumnType[]
formState: Record<string, any>
newFieldTitles?: string[]
}) => {
let defaultColumnName = FieldNameFromUITypes[formState.uidt as UITypes]
if (!defaultColumnName) {
return `title${generateUniqueColumnSuffix({ tableExplorerColumns, metaColumns })}`
}
switch (formState.uidt) {
case UITypes.User: {
if (formState.meta.is_multi) {
defaultColumnName = `${defaultColumnName}s`
}
break
}
case UITypes.Links:
case UITypes.LinkToAnotherRecord: {
if (!formState.childTableTitle) {
return `title${generateUniqueColumnSuffix({ tableExplorerColumns, metaColumns })}`
}
let childTableTitle = formState.childTableTitle
// Use plural for links except oo relation type
if (formState.uidt === UITypes.Links && formState?.type !== 'oo') {
childTableTitle = pluralize(childTableTitle)
}
// Calculate the remaining length available for childTableTitle
const maxLength = 255 - (defaultColumnName.length - 11 + '{TableName}'.length)
// Truncate childTableTitle if it exceeds the maxLength
if (childTableTitle.length > maxLength) {
childTableTitle = `${childTableTitle.slice(0, maxLength - 3)}...`
}
// Replace {TableName} with the potentially truncated childTableTitle
defaultColumnName = defaultColumnName.replace('{TableName}', childTableTitle)
// Ensure the final defaultColumnName is less than 255 characters
if (defaultColumnName.length >= 255) {
defaultColumnName = `${defaultColumnName.slice(0, 252)}...`
}
break
}
case UITypes.Lookup: {
if (!formState.lookupTableTitle || !formState.lookupColumnTitle) {
return `title${generateUniqueColumnSuffix({ tableExplorerColumns, metaColumns })}`
}
let lookupTableTitle = formState.lookupTableTitle
let lookupColumnTitle = formState.lookupColumnTitle
// Calculate the lengths of the placeholders
const placeholderLength = '{TableName}'.length + '{FieldName}'.length
const baseLength = defaultColumnName.length - placeholderLength
// Calculate the maximum length allowed for both titles combined
const maxTotalLength = 255 - baseLength
const maxLengthPerTitle = Math.floor(maxTotalLength / 2)
// Truncate the titles if necessary
if (lookupTableTitle.length > maxLengthPerTitle) {
lookupTableTitle = `${lookupTableTitle.slice(0, maxLengthPerTitle - 3)}...`
}
if (lookupColumnTitle.length > maxLengthPerTitle) {
lookupColumnTitle = `${lookupColumnTitle.slice(0, maxLengthPerTitle - 3)}...`
}
// Replace placeholders
defaultColumnName = defaultColumnName.replace('{TableName}', lookupTableTitle).replace('{FieldName}', lookupColumnTitle)
break
}
case UITypes.Rollup: {
if (!formState.rollupTableTitle || !formState.rollupColumnTitle || !formState?.rollup_function_name) {
return `title${generateUniqueColumnSuffix({ tableExplorerColumns, metaColumns })}`
}
let rollupTableTitle = formState.rollupTableTitle
let rollupColumnTitle = formState.rollupColumnTitle
// Update rollup function name
defaultColumnName = defaultColumnName.replace('{RollupFunction}', formState.rollup_function_name)
// Calculate the lengths of the placeholders
const placeholderLength = '{TableName}'.length + '{FieldName}'.length
const baseLength = defaultColumnName.length - placeholderLength
// Calculate the maximum length allowed for both titles combined
const maxTotalLength = 255 - baseLength
const maxLengthPerTitle = Math.floor(maxTotalLength / 2)
// Truncate the titles if necessary
if (rollupTableTitle.length > maxLengthPerTitle) {
rollupTableTitle = `${rollupTableTitle.slice(0, maxLengthPerTitle - 3)}...`
}
if (rollupColumnTitle.length > maxLengthPerTitle) {
rollupColumnTitle = `${rollupColumnTitle.slice(0, maxLengthPerTitle - 3)}...`
}
// Replace placeholders
defaultColumnName = defaultColumnName.replace('{TableName}', rollupTableTitle).replace('{FieldName}', rollupColumnTitle)
break
}
}
return extractNextDefaultColumnName({
tableExplorerColumns,
metaColumns,
defaultColumnName,
newFieldTitles: newFieldTitles || [],
formState,
})
}

44
packages/nocodb-sdk/src/lib/UITypes.ts

@ -90,6 +90,50 @@ export const UITypesName = {
[UITypes.LastModifiedBy]: 'Last modified by', [UITypes.LastModifiedBy]: 'Last modified by',
}; };
export const FieldNameFromUITypes = {
[UITypes.ID]: 'ID',
[UITypes.LinkToAnotherRecord]: '{TableName}',
[UITypes.ForeignKey]: 'Foreign key',
[UITypes.Lookup]: '{FieldName} (from {TableName})',
[UITypes.SingleLineText]: 'Text',
[UITypes.LongText]: 'Notes',
[UITypes.Attachment]: 'Attachment',
[UITypes.Checkbox]: 'Done',
[UITypes.MultiSelect]: 'Tags',
[UITypes.SingleSelect]: 'Status',
[UITypes.Collaborator]: 'User',
[UITypes.Date]: 'Date',
[UITypes.Year]: 'Year',
[UITypes.Time]: 'Time',
[UITypes.PhoneNumber]: 'Phone',
[UITypes.GeoData]: 'Geo data',
[UITypes.Email]: 'Email',
[UITypes.URL]: 'URL',
[UITypes.Number]: 'Number',
[UITypes.Decimal]: 'Decimal',
[UITypes.Currency]: 'Currency',
[UITypes.Percent]: 'Percent',
[UITypes.Duration]: 'Duration',
[UITypes.Rating]: 'Rating',
[UITypes.Formula]: 'Formula',
[UITypes.Rollup]: '{RollupFunction}({FieldName}) from {TableName}',
[UITypes.Count]: 'Count',
[UITypes.DateTime]: 'Date time',
[UITypes.CreatedTime]: 'Created time',
[UITypes.LastModifiedTime]: 'Last modified time',
[UITypes.AutoNumber]: 'Auto number',
[UITypes.Geometry]: 'Geometry',
[UITypes.JSON]: 'JSON',
[UITypes.SpecificDBType]: 'Specific DB type',
[UITypes.Barcode]: 'Barcode',
[UITypes.QrCode]: 'Qr code',
[UITypes.Button]: 'Button',
[UITypes.Links]: '{TableName}',
[UITypes.User]: 'User',
[UITypes.CreatedBy]: 'Created by',
[UITypes.LastModifiedBy]: 'Last modified by',
};
export const numericUITypes = [ export const numericUITypes = [
UITypes.Duration, UITypes.Duration,
UITypes.Currency, UITypes.Currency,

1
packages/nocodb-sdk/src/lib/index.ts

@ -10,6 +10,7 @@ export * from '~/lib/formulaHelpers';
export { export {
default as UITypes, default as UITypes,
UITypesName, UITypesName,
FieldNameFromUITypes,
numericUITypes, numericUITypes,
isNumericCol, isNumericCol,
isVirtualCol, isVirtualCol,

10
tests/playwright/pages/Dashboard/SurveyForm/index.ts

@ -116,9 +116,17 @@ export class SurveyFormPage extends BasePage {
await this.rootPage.waitForTimeout(100); await this.rootPage.waitForTimeout(100);
} }
async validateSuccessMessage(param: { message: string; showAnotherForm?: boolean }) { async validateSuccessMessage(param: { message: string; showAnotherForm?: boolean; isCustomMsg?: boolean }) {
await this.get().locator('[data-testid="nc-survey-form__success-msg"]').waitFor({ state: 'visible' }); await this.get().locator('[data-testid="nc-survey-form__success-msg"]').waitFor({ state: 'visible' });
if (param.isCustomMsg) {
await this.get()
.locator('[data-testid="nc-survey-form__success-msg"]')
.locator('.tiptap.ProseMirror')
.waitFor({ state: 'visible' });
}
await this.rootPage.waitForTimeout(200);
await expect( await expect(
this.get().locator(`[data-testid="nc-survey-form__success-msg"]:has-text("${param.message}")`) this.get().locator(`[data-testid="nc-survey-form__success-msg"]:has-text("${param.message}")`)
).toBeVisible(); ).toBeVisible();

3
tests/playwright/tests/db/views/viewForm.spec.ts

@ -968,6 +968,7 @@ test.describe('Form view: field validation', () => {
// validate post submit data // validate post submit data
await surveyForm.validateSuccessMessage({ await surveyForm.validateSuccessMessage({
message: 'Thank you for submitting the form', message: 'Thank you for submitting the form',
isCustomMsg: true,
}); });
}); });
@ -1306,6 +1307,7 @@ test.describe('Form view: field validation', () => {
// validate post submit data // validate post submit data
await surveyForm.validateSuccessMessage({ await surveyForm.validateSuccessMessage({
message: 'Thank you for submitting the form', message: 'Thank you for submitting the form',
isCustomMsg: true,
}); });
}); });
@ -1418,6 +1420,7 @@ test.describe('Form view: field validation', () => {
// validate post submit data // validate post submit data
await surveyForm.validateSuccessMessage({ await surveyForm.validateSuccessMessage({
message: 'Thank you for submitting the form', message: 'Thank you for submitting the form',
isCustomMsg: true,
}); });
}); });
}); });

1
tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

@ -85,6 +85,7 @@ test.describe('Share form', () => {
await surveyForm.validateSuccessMessage({ await surveyForm.validateSuccessMessage({
message: 'Thank you for submitting the form', message: 'Thank you for submitting the form',
showAnotherForm: true, showAnotherForm: true,
isCustomMsg: true,
}); });
}); });
}); });

Loading…
Cancel
Save