Something went wrong. Try again.
🔥 🔥 🔥 A Free & Self-hostable Airtable Alternative nocodb.com
Something went wrong. Try again.
9.5 kB · 302 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303import type { Ref } from 'vue'import type { RuleObject } from 'ant-design-vue/es/form'import type { ColumnType, FilterType, FormType, TableType, ViewType } from 'nocodb-sdk'import { RelationTypes, UITypes, groupFormColumnsByRow, isLinksOrLTAR } from 'nocodb-sdk'import type { ValidateInfo } from 'ant-design-vue/es/form/useForm'
const useForm = Form.useForm
const [useProvideFormViewStore, useFormViewStore] = useInjectionState( ( _meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>, viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>, formViewData: Ref<FormType | undefined>, updateFormView: (view: FormType | undefined) => Promise<any>, isEditable: boolean, ) => { const { $api } = useNuxtApp()
const { t } = useI18n()
const { appInfo } = useGlobal()
const baseStore = useBase()
const { isMysql } = useBase()
const { getMeta } = useMetas()
const formResetHook = createEventHook<void>()
const allViewFilters = ref<Record<string, FilterType[]>>({})
const formState = ref<Record<string, any>>({})
const activeRow = ref('')
const localColumns = ref<Record<string, any>[]>([])
const localColumnsMapByFkColumnId = computed(() => { return localColumns.value.reduce((acc, c) => { acc[c.fk_column_id] = c
return acc }, {} as Record<string, ColumnType & Record<string, any>>) })
const fieldVisibilityValidator = computed(() => { return new FormFilters({ nestedGroupedFilters: allViewFilters.value, formViewColumns: localColumns.value, formViewColumnsMapByFkColumnId: localColumnsMapByFkColumnId.value, formState: formState.value, isMysql, getMeta, baseId: baseStore.baseId, }) })
const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)), )
const rows = computed(() => groupFormColumnsByRow(visibleColumns.value))
const activeField = computed(() => visibleColumns.value.find((c) => c.id === activeRow.value) || null)
const activeColumn = computed(() => { if (_meta.value && activeField.value) { if (_meta.value.columnsById && (_meta.value.columnsById as Record<string, ColumnType>)[activeField.value?.fk_column_id]) { return (_meta.value.columnsById as Record<string, ColumnType>)[activeField.value.fk_column_id] } else if (_meta.value.columns) { return _meta.value.columns.find((c) => c.id === activeField.value?.fk_column_id) ?? null } } return null })
const fieldMappings = computed(() => { const uniqueFieldNames: Set<string> = new Set()
return visibleColumns.value.reduce((acc, c) => { acc[c.title] = getValidFieldName(c.title, uniqueFieldNames) return acc }, {} as Record<string, string>) })
const validators = computed(() => { const rulesObj: Record<string, RuleObject[]> = {}
if (!visibleColumns.value || !Object.keys(fieldMappings.value).length) return rulesObj
for (const column of visibleColumns.value) { let rules: RuleObject[] = [ { validator: (_rule: RuleObject, value: any) => { return new Promise((resolve, reject) => { if (isRequired(column, column.required) && column.visible) { if (typeof value === 'string') { value = value.trim() }
if ( (column.uidt === UITypes.Checkbox && !value) || (column.uidt !== UITypes.Checkbox && !requiredFieldValidatorFn(value)) ) { return reject(t('msg.error.fieldRequired')) } }
return resolve() }) }, }, { validator: (_rule: RuleObject) => { return new Promise((resolve) => { checkFieldVisibility() return resolve() }) }, }, ]
const additionalRules = extractFieldValidator( parseProp(column.meta).validators ?? [], column, appInfo.value.ncMaxTextLength, ) rules = [...rules, ...additionalRules]
if (rules.length) { rulesObj[fieldMappings.value[column.title!]] = rules } }
return rulesObj })
const fieldMappingFormState = computed(() => { if (!Object.keys(fieldMappings.value).length) return {}
return Object.keys(formState.value).reduce((acc, key) => { acc[fieldMappings.value[key]] = formState.value[key] return acc }, {} as Record<string, any>) })
// Form field validation const { validate, validateInfos, clearValidate } = useForm(fieldMappingFormState, validators)
const validateActiveField = async (col: ColumnType) => { if (!col.title) return
if (fieldMappings.value[col.title] === undefined) { console.warn('Missing mapping field for:', col.title) return }
try { await validate(fieldMappings.value[col.title]) } catch {} }
const isValidRedirectUrl = (): ValidateInfo => { if (typeof formViewData.value?.redirect_url !== 'string') return { validateStatus: '', help: undefined }
const url = formViewData.value?.redirect_url?.trim() ?? ''
if (!url.startsWith('http://') && !url.startsWith('https://')) { return { validateStatus: 'error', help: 'Redirect url must starts with `http://` or `https://`', } }
return { validateStatus: '', help: undefined } }
const updateView = useDebounceFn( () => { if (isValidRedirectUrl().validateStatus === 'error') { formViewData.value = { ...formViewData.value, redirect_url: '', } }
updateFormView(formViewData.value) }, 300, { maxWait: 2000 }, )
const updateColMeta = useDebounceFn(async (col: Record<string, any>) => { if (col?.id && isEditable) { validateActiveField(col)
try { await $api.internal.postOperation( viewMeta.value!.fk_workspace_id!, viewMeta.value!.base_id!, { operation: 'formColumnUpdate', formColumnId: col.id }, pickFormColumnUpdateBody(col), ) } catch (e: any) { message.error(await extractSdkResponseErrorMsg(e)) } } }, 250)
/** * Atomically re-layout multiple form columns after a drag-drop reflow. * Each update carries the new `row_id` and/or `order` for a column. * Throws on API failure so callers can roll back optimistic UI state. */ async function bulkUpdateColumns(updates: Array<{ id: string; row_id?: string | null; order?: number }>) { if (!isEditable || !updates.length || !viewMeta.value?.id) return
await $api.internal.postOperation( viewMeta.value.fk_workspace_id!, viewMeta.value.base_id!, { operation: 'formColumnBulkUpdate', viewId: viewMeta.value.id }, { updates }, ) }
function isRequired(_columnObj: Record<string, any>, required = false) { let columnObj = _columnObj if (isLinksOrLTAR(columnObj.uidt) && columnObj.colOptions && columnObj.colOptions.type === RelationTypes.BELONGS_TO) { columnObj = (_meta?.value?.columns || []).find( (c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id, ) as Record<string, any> }
return required || (columnObj && columnObj.rqd && !columnObj.cdf) }
const loadAllviewFilters = async () => { if (!viewMeta.value?.id) return try { const formViewFilters = ( await $api.internal.getOperation(viewMeta.value!.fk_workspace_id!, viewMeta.value!.base_id!, { operation: 'filterList', viewId: viewMeta.value.id, includeAllFilters: true, }) ).list || []
if (!formViewFilters.length) return
const formFilter = new FormFilters({ data: formViewFilters, baseId: viewMeta.value.base_id })
const allFilters = formFilter.getNestedGroupedFilters()
allViewFilters.value = { ...allFilters } } catch (e: any) { console.error('Error loading view filters:', e) } }
async function checkFieldVisibility() { await fieldVisibilityValidator.value.validateVisibility() }
return { onReset: formResetHook.on, formState, localColumns, visibleColumns, rows, activeRow, activeField, activeColumn, isRequired, updateView, updateColMeta, bulkUpdateColumns, validate, validateInfos, clearValidate, fieldMappings, isValidRedirectUrl, formViewData, loadAllviewFilters, allViewFilters, localColumnsMapByFkColumnId, checkFieldVisibility, } }, 'form-view-store',)
export { useProvideFormViewStore }
export function useFormViewStoreOrThrow() { const sharedFormStore = useFormViewStore()
if (sharedFormStore == null) throw new Error('Please call `useProvideFormViewStore` on the appropriate parent component')
return sharedFormStore}