Something went wrong. Try again.
🔥 🔥 🔥 A Free & Self-hostable Airtable Alternative nocodb.com
Something went wrong. Try again.
22 kB · 627 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628import type { ColumnReqType, ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'import { ButtonActionsType, UITypes, isAIPromptCol, isCreatedOrLastModifiedTimeCol, isLinksOrLTAR, isMMOrMMLike, isSystemColumn,} from 'nocodb-sdk'import type { Ref } from 'vue'import type { RuleObject } from 'ant-design-vue/es/form'import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers'
const useForm = Form.useForm
interface ValidationsObj { [key: string]: RuleObject[]}
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( ( meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>, tableExplorerColumns?: Ref<ColumnType[] | undefined>, fromTableExplorer?: Ref<boolean | undefined>, isColumnValid?: Ref<((value: Partial<ColumnType>) => boolean) | undefined>, fromKanbanStack?: Ref<boolean | undefined>, ) => { const baseStore = useBase()
const { isMysql: isMysqlFunc, isPg: isPgFunc, isXcdbBase: isXcdbBaseFunc } = baseStore
const { sqlUis } = storeToRefs(baseStore)
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
const { isMetaReadOnly } = useRoles()
const { t } = useI18n()
const { $e } = useNuxtApp()
const sqlUi = computed(() => (meta.value?.source_id ? sqlUis.value[meta.value?.source_id] : Object.values(sqlUis.value)[0]))
const viewsStore = useViewsStore()
const { activeView } = storeToRefs(viewsStore)
const { xWhere, view, eventBus } = useSmartsheetStoreOrThrow()
const { formattedData, loadData } = useViewData(meta, view, xWhere)
const { isAiModeFieldModal, activeTabSelectedFields } = usePredictFields(ref(false))
const disableSubmitBtn = ref(false)
const isSaving = ref(false)
const isWebhookCreateModalOpen = ref(false)
const isScriptCreateModalOpen = ref(false)
const isAiButtonConfigModalOpen = ref(false)
const isConvertLinkV2ModalOpen = ref(false)
const isEdit = computed(() => !!column?.value?.id)
const isMysql = computed(() => isMysqlFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]))
const isPg = computed(() => isPgFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]))
const isSystem = computed(() => isSystemColumn(column.value))
const isSyncedField = computed(() => meta.value?.synced && column?.value?.readonly && !isAutoGeneratedColumn(column?.value))
const isXcdbBase = computed(() => isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]), )
const idType = null
const additionalValidations = ref<ValidationsObj>({})
const avoidShowingToastMsgForValidations = ref<{ [key: string]: boolean }>({})
const setAdditionalValidations = (validations: ValidationsObj) => { additionalValidations.value = { ...additionalValidations.value, ...validations } }
const removeAdditionalValidation = (key: string) => { delete additionalValidations.value[key] }
const setAvoidShowingToastMsgForValidations = (validations: { [key: string]: boolean }) => { avoidShowingToastMsgForValidations.value = { ...avoidShowingToastMsgForValidations.value, ...validations } } const defaultType = isMetaReadOnly.value ? UITypes.Formula : UITypes.SingleLineText
const defaultFormState = { title: '', description: '', uidt: null, custom: {}, }
const formState = ref<Record<string, any>>({ ...defaultFormState, uidt: fromTableExplorer?.value ? defaultType : null, ...deepClone(column.value || {}), })
const isAiMode = computed(() => { if (formState.value.uidt === UITypes.Button && formState.value.type === ButtonActionsType.Ai) { return true }
if (isAIPromptCol(formState.value)) { return true }
return false })
const onUidtOrIdTypeChange = (preload?: Record<string, any>) => { disableSubmitBtn.value = false
const newTitle = updateFieldName(false, preload)
const colProp = sqlUi.value?.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined) ?? {}
formState.value = { ...(fromTableExplorer?.value || formState.value?.is_ai_field || formState.value?.ai_temp_id ? { is_ai_field: formState.value?.is_ai_field, ai_temp_id: formState.value?.ai_temp_id, view_id: formState.value?.view_id, description: formState.value?.description, } : {}), custom: {}, ...(!isEdit.value && { // 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 mess up the column creation title: newTitle || formState.value.title, column_name: newTitle || formState.value.column_name, uidt: formState.value.uidt, temp_id: formState.value.temp_id, userHasChangedTitle: !!formState.value?.userHasChangedTitle, }), ...(isEdit.value && { // take the existing formState.value when editing a column // LTAR is not available in this case ...formState.value, }), meta: {}, rqd: false, pk: false, ai: false, cdf: null, un: false, dtx: 'specificType', ...colProp, }
if (preload) { formState.value = { ...formState.value, ...preload } }
formState.value.dtxp = sqlUi.value?.getDefaultLengthForDatatype(formState.value.dt) ?? null formState.value.dtxs = sqlUi.value?.getDefaultScaleForDatatype(formState.value.dt) ?? null
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect] if (column && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column.value?.uidt as UITypes)) { formState.value.dtxp = column.value?.dtxp }
if (columnToValidate.includes(formState.value.uidt)) { formState.value.meta = { validate: formState.value.meta && formState.value.meta.validate, } }
// keep length and scale for same datatype if (column.value && formState.value.uidt === column.value?.uidt) { formState.value.dtxp = column.value.dtxp formState.value.dtxs = column.value.dtxs } else { // default length and scale for currency if (formState.value?.uidt === UITypes.Currency) { formState.value.dtxp = 19 formState.value.dtxs = 2 } }
formState.value.altered = formState.value.altered || 2 }
// actions const generateNewColumnMeta = (ignoreUidt = false) => { setAdditionalValidations({}) setAvoidShowingToastMsgForValidations({}) formState.value = { meta: {}, ...(sqlUi.value?.getNewColumn(1) ?? {}), } formState.value.title = '' formState.value.column_name = ''
if (isMetaReadOnly.value) { formState.value.uidt = defaultType onUidtOrIdTypeChange() } if (ignoreUidt && !fromTableExplorer?.value) { formState.value.uidt = null } }
const validators = computed(() => { return { title: [ ...(isEdit.value ? [ { required: true, message: t('msg.error.columnNameRequired'), }, ] : []), // validation for unique column name { validator: (rule: any, value: any) => { return new Promise<void>((resolve, reject) => { if ( value !== '' && (tableExplorerColumns?.value || meta.value?.columns)?.some( (c) => c.id !== formState.value.id && // ignore current column // compare against column_name and title ((value || '').toLowerCase() === (c.column_name || '').toLowerCase() || (value || '').toLowerCase() === (c.title || '').toLowerCase()) && c.system, ) ) { return reject(new Error(t('msg.error.duplicateSystemColumnName'))) }
const isAiFieldExist = isAiModeFieldModal.value ? activeTabSelectedFields.value.some((c) => { return ( c.ai_temp_id !== formState.value?.ai_temp_id && ((value || '').trim().toLowerCase() === (c.formState?.column_name || '').trim().toLowerCase() || (value || '').trim().toLowerCase() === (c.formState?.title || '').trim().toLowerCase() || (value || '').trim().toLowerCase() === (c?.title || '').trim().toLowerCase()) ) }) : false
if ( value !== '' && ((tableExplorerColumns?.value || meta.value?.columns)?.some( (c) => c.id !== formState.value.id && // ignore current column // compare against column_name and title ((value || '').trim().toLowerCase() === (c.column_name || '').trim().toLowerCase() || (value || '').trim().toLowerCase() === (c.title || '').trim().toLowerCase()), ) || isAiFieldExist) ) { return reject(new Error(t('msg.error.duplicateColumnName'))) } resolve() }) }, }, fieldLengthValidator(), ], uidt: [ { required: true, message: t('msg.error.uiDataTypeRequired'), }, ], cdf: [ { validator: (rule: any, value: any) => { return new Promise<void>((resolve, reject) => { const columnValidationError = getColumnValidationError(formState.value, value) if (columnValidationError) { return reject(new Error(t(columnValidationError))) }
resolve() }) }, }, ], ...(additionalValidations?.value || {}), } })
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const onDataTypeChange = () => { formState.value.rqd = false if (formState.value.uidt !== UITypes.ID) { formState.value.primaryKey = false } formState.value.ai = false formState.value.cdf = null formState.value.un = false formState.value.dtxp = sqlUi.value?.getDefaultLengthForDatatype(formState.value.dt) ?? null formState.value.dtxs = sqlUi.value?.getDefaultScaleForDatatype(formState.value.dt) ?? null
formState.value.dtx = 'specificType'
// use enum response as dtxp for select columns const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect] if (column.value && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column.value.uidt as UITypes)) { formState.value.dtxp = column.value.dtxp }
// keep length and scale for same datatype if (column.value && formState.value.uidt === column.value?.uidt) { formState.value.dtxp = column.value.dtxp formState.value.dtxs = column.value.dtxs } else { // default length and scale for currency if (formState.value?.uidt === UITypes.Currency) { formState.value.dtxp = 19 formState.value.dtxs = 2 } }
// this.$set(formState.value, 'uidt', sqlUi.value.getUIType(formState.value));
formState.value.altered = formState.value.altered || 2 }
// todo: type of onAlter is wrong, the first argument is `CheckboxChangeEvent` not a number. const onAlter = (val = 2, cdf = false) => { formState.value.altered = formState.value.altered || val if (cdf) formState.value.cdf = formState.value.cdf || null }
const addOrUpdate = async ( onSuccess: (col?: ColumnType) => Promise<void>, columnPosition?: Pick<ColumnReqType, 'column_order'>, ) => { try { if (!(await validate())) return } catch (e: any) { let skipToast = false const errorMsgs = (e?.errorFields || []) .filter((f) => { if (avoidShowingToastMsgForValidations.value[f?.name ?? '']) { skipToast = true }
return f?.name !== 'cdf' && !avoidShowingToastMsgForValidations.value[f?.name ?? ''] }) .map((e: any) => e.errors?.join(', ')) .filter(Boolean) .join(', ')
if (errorMsgs) { message.error(errorMsgs) return }
if (skipToast) return
if (!fromKanbanStack?.value || (fromKanbanStack.value && !e.outOfDate)) { message.error(t('msg.error.formValidationFailed')) return } }
let savedColumn: ColumnType | undefined let oldCol: ColumnType | undefined
try { isSaving.value = true // set saving state
// trim title before saving if (formState.value.title) { formState.value.title = formState.value.title.trim() }
formState.value.table_name = meta.value?.table_name
const refModelId = formState.value.custom?.ref_model_id
// formState.value.title = formState.value.column_name if (column.value) { // reset column validation if column is not to be validated if (!columnToValidate.includes(formState.value.uidt)) { formState.value.validate = '' }
let updateData: typeof formState.value = formState.value
// For system datetime fields, only send meta and description // to avoid triggering the system field non-modifiable check if (isSystem.value && isCreatedOrLastModifiedTimeCol(column.value)) { updateData = { meta: updateData.meta, description: updateData.description, } as typeof updateData }
if (isSyncedField.value) { updateData = { meta: updateData.meta, description: updateData.description, } as typeof updateData }
try { oldCol = column.value await $api.internal.postOperation( meta.value!.fk_workspace_id!, meta.value!.base_id!, { operation: 'columnUpdate', columnId: column.value?.id as string, }, updateData, )
// if LTARv2 column update and relation type changed // then reload the reference table meta if (isMMOrMMLike(column.value)) { getMeta( (column.value?.colOptions as LinkToAnotherRecordType)?.fk_related_base_id ?? column.value?.base_id, (column.value?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id, true, ) } else if ( // text → link conversion: the backend creates the opposite link // column on the related (child) table. The check above keys off the // *old* column, which is still SingleLineText here, so force-reload // the related table's (now stale) cached meta — mirrors the columnAdd // branch below. Without this the opposite field never shows up on the // target table until a hard refresh. isLinksOrLTAR(formState.value) && !isLinksOrLTAR(column.value) && formState.value.childId && meta.value?.id !== formState.value.childId ) { getMeta(formState.value.ref_base_id ?? meta.value!.base_id!, formState.value.childId, true) }
if (oldCol && [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(oldCol.uidt)) { viewsStore.loadViews({ tableId: oldCol?.fk_model_id, baseId: meta.value!.base_id!, ignoreLoading: true, force: true, }) } eventBus.emit(SmartsheetStoreEvents.FIELD_UPDATE) eventBus.emit(SmartsheetStoreEvents.ROW_COLOR_UPDATE) } catch (e: any) { if (!validateInfos.formula_raw) validateInfos.formula_raw = {} validateInfos.formula_raw!.validateStatus = 'error' if (!validateInfos.formula_raw?.help) { validateInfos.formula_raw!.help = [] } validateInfos.formula_raw?.help.push(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e)) return }
if (meta.value?.id && column.value.uidt === UITypes.Attachment && column.value.uidt !== formState.value.uidt) { viewsStore.updateViewCoverImageColumnId({ metaId: meta.value.id as string, baseId: meta.value.base_id, columnIds: new Set([column.value.id as string]), }) }
// Column updated // message.success(t('msg.success.columnUpdated')) } 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 if (formState.value.uidt === UITypes.ID) { // based on id column type set autogenerated meta prop // if (isAutoGenId) { // this.newColumn.meta = { // ag: 'nc', // }; // } } const tableMeta = await $api.internal.postOperation( meta.value!.fk_workspace_id!, meta.value!.base_id!, { operation: 'columnAdd', tableId: meta.value?.id as string, }, { ...formState.value, ...columnPosition, view_id: activeView.value!.id as string, }, )
savedColumn = tableMeta.columns?.find( (c) => c.title === formState.value.title || c.column_name === formState.value.column_name, )
/** if LTAR column then force reload related table meta */ if (isLinksOrLTAR(formState.value) && meta.value?.id !== formState.value.childId) { const relatedBaseId = (savedColumn?.colOptions as any)?.fk_related_base_id || meta.value!.base_id if (refModelId) { getMeta(relatedBaseId!, refModelId, true).then(() => {}) } else { getMeta(relatedBaseId!, formState.value.childId, true).then(() => {}) } }
// Column created // message.success(t('msg.success.columnCreated')) $e('a:column:add', { datatype: formState.value.uidt }) } await onSuccess?.(savedColumn) return true } catch (e: any) { message.error(await extractSdkResponseErrorMsg(e)) } finally { isSaving.value = false // reset saving state } }
function updateFieldName(updateFormState = true, preload?: Record<string, any>, force = false) { if ( formState.value?.is_ai_field || isEdit.value || !fromTableExplorer?.value || formState.value?.userHasChangedTitle || (!isColumnValid?.value?.(formState.value) && !force) ) { return }
const defaultColumnName = generateUniqueColumnName({ formState: { ...formState.value, ...(preload ?? {}) }, 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 */ watch( () => formState.value?.title, (newTitle) => (formState.value.column_name = newTitle), )
return { formState, generateNewColumnMeta, addOrUpdate, onAlter, onDataTypeChange, onUidtOrIdTypeChange, setAdditionalValidations, removeAdditionalValidation, setAvoidShowingToastMsgForValidations, resetFields, validate, validateInfos, isEdit, column, sqlUi, isPg, isWebhookCreateModalOpen, isAiButtonConfigModalOpen, isConvertLinkV2ModalOpen, isMysql, isSystem, isXcdbBase, disableSubmitBtn, updateFieldName, fromTableExplorer, isAiMode, formattedData, loadData, tableExplorerColumns, defaultFormState, isScriptCreateModalOpen, isSaving, isSyncedField, } },)
export { useProvideColumnCreateStore }
export function useColumnCreateStoreOrThrow() { const columnCreateStore = useColumnCreateStore()
if (columnCreateStore == null) throw new Error('Please call `useProvideColumnCreateStore` on the appropriate parent component')
return columnCreateStore}