import useVuelidate from '@vuelidate/core' import { helpers, minLength, required } from '@vuelidate/validators' import type { Ref } from 'vue' import type { BoolType, ColumnType, FormColumnType, FormType, LinkToAnotherRecordType, StringOrNullType, TableType, ViewType, } from 'nocodb-sdk' import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { isString } from '@vueuse/core' import { SharedViewPasswordInj, computed, createEventHook, extractSdkResponseErrorMsg, message, provide, ref, storeToRefs, useApi, useI18n, useInjectionState, useMetas, useProject, useProvideSmartsheetRowStore, watch, } from '#imports' import type { SharedViewMeta } from '~/lib' const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((sharedViewId: string) => { const progress = ref(false) const notFound = ref(false) const submitted = ref(false) const passwordDlg = ref(false) const password = ref(null) const passwordError = ref(null) const secondsRemain = ref(0) provide(SharedViewPasswordInj, password) const sharedView = ref() const sharedFormView = ref() const meta = ref() const columns = ref<(ColumnType & { required?: BoolType; show?: BoolType; label?: StringOrNullType; enable_scanner?: BoolType })[]>() const sharedViewMeta = ref({}) const formResetHook = createEventHook() const { api, isLoading } = useApi() const { metas, setMeta } = useMetas() const projectStore = useProject() const { project } = storeToRefs(projectStore) const { t } = useI18n() const formState = ref>({}) const { state: additionalState } = useProvideSmartsheetRowStore( meta as Ref, ref({ row: formState, rowMeta: { new: true }, oldRow: {}, }), ) const fieldRequired = (fieldName = 'Value') => helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required) const formColumns = computed(() => columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || col.uidt === UITypes.LinkToAnotherRecord), ) const loadSharedView = async () => { passwordError.value = null try { const viewMeta = await api.public.sharedViewMetaGet(sharedViewId, { headers: { 'xc-password': password.value, }, }) passwordDlg.value = false sharedView.value = viewMeta sharedFormView.value = viewMeta.view meta.value = viewMeta.model const fieldById = (viewMeta.columns || []).reduce( (o: Record, f: Record) => ({ ...o, [f.fk_column_id]: f, }), {} as Record, ) let order = 1 columns.value = meta?.value?.columns ?.map((c: Record) => ({ ...c, fk_column_id: c.id, fk_view_id: viewMeta.id, ...(fieldById[c.id] ? fieldById[c.id] : {}), order: (fieldById[c.id] && fieldById[c.id].order) || order++, id: fieldById[c.id] && fieldById[c.id].id, })) .sort((a: Record, b: Record) => a.order - b.order) as Record[] const _sharedViewMeta = (viewMeta as any).meta sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta await setMeta(viewMeta.model) // if project is not defined then set it with an object containing base if (!project.value?.bases) projectStore.setProject({ bases: [ { id: viewMeta.base_id, type: viewMeta.client, }, ], }) const relatedMetas = { ...viewMeta.relatedMetas } Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key])) } catch (e: any) { if (e.response && e.response.status === 404) { notFound.value = true } else if ((await extractSdkResponseErrorMsg(e)) === ErrorMessages.INVALID_SHARED_VIEW_PASSWORD) { passwordDlg.value = true if (password.value && password.value !== '') passwordError.value = 'Something went wrong. Please check your credentials.' } } } const validators = computed(() => { const obj: Record> = { localState: {}, virtual: {}, } if (!formColumns.value) return obj for (const column of formColumns.value) { if ( !isVirtualCol(column) && ((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required) ) { obj.localState[column.title!] = { required: fieldRequired(column.label || column.title) } } else if ( column.uidt === UITypes.LinkToAnotherRecord && column.colOptions && (column.colOptions as LinkToAnotherRecordType).type === RelationTypes.BELONGS_TO ) { const col = columns.value?.find((c) => c.id === (column?.colOptions as LinkToAnotherRecordType)?.fk_child_column_id) if ((col && col.rqd && !col.cdf) || column.required) { if (col) { obj.virtual[column.title!] = { required: fieldRequired(column.label || column.title) } } } } else if (isVirtualCol(column) && column.required) { obj.virtual[column.title!] = { minLength: minLength(1), required: fieldRequired(column.label || column.title), } } } return obj }) const v$ = useVuelidate( validators, computed(() => ({ localState: formState.value, virtual: additionalState.value })), ) const submitForm = async () => { try { if (!(await v$.value?.$validate())) { return } progress.value = true const data: Record = { ...formState.value, ...additionalState.value } const attachment: Record = {} /** find attachments in form data */ for (const col of metas.value?.[sharedView.value?.fk_model_id as string]?.columns) { if (col.uidt === UITypes.Attachment) { if (data[col.title]) { attachment[`_${col.title}`] = data[col.title].map((item: { file: File }) => item.file) } } } await api.public.dataCreate( sharedView.value!.uuid!, { data, ...attachment, }, { headers: { 'xc-password': password.value, }, }, ) submitted.value = true progress.value = false await message.success(sharedFormView.value?.success_msg || 'Saved successfully.') } catch (e: any) { console.log(e) await message.error(await extractSdkResponseErrorMsg(e)) } progress.value = false } /** reset form if show_blank_form is true */ watch(submitted, (nextVal) => { if (nextVal && sharedFormView.value?.show_blank_form) { secondsRemain.value = 5 const intvl = setInterval(() => { secondsRemain.value = secondsRemain.value - 1 if (secondsRemain.value < 0) { submitted.value = false formResetHook.trigger() clearInterval(intvl) } }, 1000) } /** reset form state and validation */ if (!nextVal) { additionalState.value = {} formState.value = {} v$.value?.$reset() } }) watch(password, (next, prev) => { if (next !== prev && passwordError.value) passwordError.value = null }) return { sharedView, sharedFormView, loadSharedView, columns, submitForm, progress, meta, validators, v$, formColumns, formState, notFound, password, passwordError, submitted, secondsRemain, passwordDlg, isLoading, sharedViewMeta, onReset: formResetHook.on, } }, 'expanded-form-store') export { useProvideSharedFormStore } export function useSharedFormStoreOrThrow() { const sharedFormStore = useSharedFormStore() if (sharedFormStore == null) throw new Error('Please call `useProvideSharedFormStore` on the appropriate parent component') return sharedFormStore }