Form.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. <!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { isEqual, cloneDeep, merge, isEmpty } from 'lodash-es'
  4. import type { ConcreteComponent, Ref } from 'vue'
  5. import {
  6. computed,
  7. ref,
  8. nextTick,
  9. shallowRef,
  10. reactive,
  11. toRef,
  12. watch,
  13. markRaw,
  14. useSlots,
  15. } from 'vue'
  16. import { FormKit, FormKitSchema } from '@formkit/vue'
  17. import type {
  18. FormKitPlugin,
  19. FormKitSchemaNode,
  20. FormKitSchemaCondition,
  21. FormKitNode,
  22. FormKitClasses,
  23. FormKitSchemaDOMNode,
  24. FormKitSchemaComponent,
  25. FormKitMessageProps,
  26. } from '@formkit/core'
  27. import { createMessage, getNode, reset } from '@formkit/core'
  28. import type { Except, SetRequired } from 'type-fest'
  29. import { refDebounced, watchOnce } from '@vueuse/shared'
  30. import getUuid from '@shared/utils/getUuid'
  31. import log from '@shared/utils/log'
  32. import { camelize } from '@shared/utils/formatter'
  33. import UserError from '@shared/errors/UserError'
  34. import type {
  35. EnumObjectManagerObjects,
  36. EnumFormUpdaterId,
  37. FormUpdaterRelationField,
  38. FormUpdaterQuery,
  39. FormUpdaterQueryVariables,
  40. ObjectAttributeValue,
  41. } from '@shared/graphql/types'
  42. import { QueryHandler } from '@shared/server/apollo/handler'
  43. import { useObjectAttributeLoadFormFields } from '@shared/entities/object-attributes/composables/useObjectAttributeLoadFormFields'
  44. import { useObjectAttributeFormFields } from '@shared/entities/object-attributes/composables/useObjectAttributeFormFields'
  45. import testFlags from '@shared/utils/testFlags'
  46. import { edgesToArray } from '@shared/utils/helpers'
  47. import type { FormUpdaterTrigger } from '@shared/types/form'
  48. import type { EntityObject } from '@shared/types/entity'
  49. import { getFirstFocusableElement } from '@shared/utils/getFocusableElements'
  50. import { parseGraphqlId } from '@shared/graphql/utils'
  51. import { useFormUpdaterQuery } from './graphql/queries/formUpdater.api'
  52. import { FormHandlerExecution, FormValidationVisibility } from './types'
  53. import type {
  54. ChangedField,
  55. FormData,
  56. FormFieldAdditionalProps,
  57. FormFieldValue,
  58. FormHandler,
  59. FormHandlerFunction,
  60. FormSchemaField,
  61. FormSchemaLayout,
  62. FormSchemaNode,
  63. FormValues,
  64. ReactiveFormSchemData,
  65. } from './types'
  66. import FormLayout from './FormLayout.vue'
  67. import FormGroup from './FormGroup.vue'
  68. export interface Props {
  69. id?: string
  70. schema?: FormSchemaNode[]
  71. formUpdaterId?: EnumFormUpdaterId
  72. handlers?: FormHandler[]
  73. changeFields?: Record<string, Partial<FormSchemaField>>
  74. // Maybe in the future this is no longer needed, when FormKit supports group
  75. // without value grouping below group name (https://github.com/formkit/formkit/issues/461).
  76. flattenFormGroups?: string[]
  77. schemaData?: Except<ReactiveFormSchemData, 'fields'>
  78. formKitPlugins?: FormKitPlugin[]
  79. formKitSectionsSchema?: Record<
  80. string,
  81. Partial<FormKitSchemaNode> | FormKitSchemaCondition
  82. >
  83. class?: FormKitClasses | string | Record<string, boolean>
  84. // Can be used to define initial values on frontend side and fetched schema from the server.
  85. initialValues?: Partial<FormValues>
  86. initialEntityObject?: EntityObject
  87. queryParams?: Record<string, unknown>
  88. validationVisibility?: FormValidationVisibility
  89. disabled?: boolean
  90. autofocus?: boolean
  91. // Some special properties for working with object attribute fields inside of a form schema.
  92. useObjectAttributes?: boolean
  93. objectAttributeSkippedFields?: string[]
  94. // Implement the submit in this way, because we need to react on async usage of the submit function.
  95. // Don't forget that to submit a form with "Enter" key, you need to add a button with type="submit" inside of the form.
  96. // Or to have a button outside of form with "form" attribite with the same value as the form id.
  97. // After this method is called, form resets its values and state. If you need to call something afterwards,
  98. // like make route navigation, you can return a function from the submit handler, which will be called after the form reset.
  99. onSubmit?: (
  100. values: FormData,
  101. ) => Promise<void | (() => void)> | void | (() => void)
  102. }
  103. // Zammad currently expects formIds to be BigInts. Maybe convert to UUIDs later.
  104. // const formId = `form-${getUuid()}`
  105. // This is the formId generation logic from the legacy desktop app.
  106. let formId = new Date().getTime() + Math.floor(Math.random() * 99999).toString()
  107. formId = formId.substr(formId.length - 9, 9)
  108. const props = withDefaults(defineProps<Props>(), {
  109. schema: () => {
  110. return []
  111. },
  112. changeFields: () => {
  113. return reactive({})
  114. },
  115. validationVisibility: FormValidationVisibility.Submit,
  116. useObjectAttributes: false,
  117. })
  118. const slots = useSlots()
  119. const hasSchema = computed(
  120. () => Boolean(slots.default) || Boolean(props.schema),
  121. )
  122. const formSchemaInitialized = ref(false)
  123. if (!hasSchema.value) {
  124. log.error(
  125. 'No schema defined. Please use the schema prop or the default slot for the schema.',
  126. )
  127. }
  128. // Rename prop 'class' for usage in the template, because of reserved word
  129. const localClass = toRef(props, 'class')
  130. const emit = defineEmits<{
  131. (
  132. e: 'changed',
  133. fieldName: string,
  134. newValue: FormFieldValue,
  135. oldValue: FormFieldValue,
  136. ): void
  137. (e: 'node', node: FormKitNode): void
  138. (e: 'settled'): void
  139. }>()
  140. const showInitialLoadingAnimation = ref(false)
  141. const debouncedShowInitialLoadingAnimation = refDebounced(
  142. showInitialLoadingAnimation,
  143. 300,
  144. )
  145. const formKitInitialNodesSettled = ref(false)
  146. const formNode: Ref<FormKitNode | undefined> = ref()
  147. const formElement = ref<HTMLElement>()
  148. const changeFields = toRef(props, 'changeFields')
  149. const updaterChangedFields = new Set<string>()
  150. const autofocusFirstInput = () => {
  151. nextTick(() => {
  152. const firstInput = getFirstFocusableElement(formElement.value)
  153. firstInput?.focus()
  154. firstInput?.scrollIntoView({ block: 'nearest' })
  155. })
  156. }
  157. const setFormNode = (node: FormKitNode) => {
  158. formNode.value = node
  159. // Save the initial entity object in the form node context, so that fields can use it.
  160. if (node.context && props.initialEntityObject) {
  161. node.context.initialEntityObject = props.initialEntityObject
  162. }
  163. node.settled.then(() => {
  164. showInitialLoadingAnimation.value = false
  165. formKitInitialNodesSettled.value = true
  166. // Reset directly after the initial request.
  167. updaterChangedFields.clear()
  168. const formName = node.context?.id || node.name
  169. testFlags.set(`${formName}.settled`)
  170. emit('settled')
  171. if (props.autofocus) autofocusFirstInput()
  172. })
  173. node.on('autofocus', autofocusFirstInput)
  174. emit('node', node)
  175. }
  176. const formNodeContext = computed(() => formNode.value?.context)
  177. // Build the flat value when its requested for specific form groups.
  178. const getFlatValues = (values: FormValues, formGroups: string[]) => {
  179. const flatValues = {
  180. ...values,
  181. }
  182. formGroups.forEach((formGroup) => {
  183. Object.assign(flatValues, flatValues[formGroup])
  184. delete flatValues[formGroup]
  185. })
  186. return flatValues
  187. }
  188. // Use the node context value, instead of the v-model, because of performance reason.
  189. const values = computed<FormValues>(() => {
  190. if (!formNodeContext.value) {
  191. return {}
  192. }
  193. if (!props.flattenFormGroups) return formNodeContext.value.value
  194. return getFlatValues(formNodeContext.value.value, props.flattenFormGroups)
  195. })
  196. const relationFields: FormUpdaterRelationField[] = []
  197. const relationFieldBelongsToObjectField: Record<string, string> = {}
  198. const formUpdaterProcessing = computed(
  199. () => formNode.value?.context?.state.formUpdaterProcessing || false,
  200. )
  201. let delayedSubmit = false
  202. const onSubmitRaw = () => {
  203. if (formUpdaterProcessing.value) {
  204. delayedSubmit = true
  205. }
  206. }
  207. const onSubmit = (values: FormData) => {
  208. // Needs to be checked, because the 'onSubmit' function is not required.
  209. if (!props.onSubmit) return undefined
  210. const flatValues = props.flattenFormGroups
  211. ? getFlatValues(values, props.flattenFormGroups)
  212. : values
  213. const emitValues = {
  214. ...flatValues,
  215. formId,
  216. }
  217. const submitResult = props.onSubmit(emitValues)
  218. if (submitResult instanceof Promise) {
  219. return submitResult
  220. .then((afterReset) => {
  221. // it's possible to destroy Form before this is called
  222. if (!formNode.value) return
  223. reset(formNode.value, values)
  224. if (typeof afterReset === 'function') afterReset()
  225. })
  226. .catch((errors: UserError) => {
  227. if (errors instanceof UserError) {
  228. formNode.value?.setErrors(
  229. // TODO: we need to check/style the general error output when we want to show it related to the form
  230. errors.generalErrors as string[],
  231. errors.getFieldErrorList(),
  232. )
  233. }
  234. })
  235. }
  236. if (formNode.value) {
  237. reset(formNode.value, values)
  238. }
  239. if (typeof submitResult === 'function') {
  240. submitResult()
  241. }
  242. return submitResult
  243. }
  244. let formUpdaterQueryHandler: QueryHandler<
  245. FormUpdaterQuery,
  246. FormUpdaterQueryVariables
  247. >
  248. const delayedSubmitPlugin = (node: FormKitNode) => {
  249. node.on('message-removed', async ({ payload }) => {
  250. if (payload.key === 'formUpdaterProcessing' && delayedSubmit) {
  251. // We need to wait on the "next tick", so that the validation for updated fields is ready.
  252. setTimeout(() => {
  253. delayedSubmit = false
  254. node.submit()
  255. }, 0)
  256. }
  257. })
  258. return false
  259. }
  260. const localFormKitPlugins = computed(() => {
  261. return [delayedSubmitPlugin, ...(props.formKitPlugins || [])]
  262. })
  263. const formConfig = computed(() => {
  264. return {
  265. validationVisibility: props.validationVisibility,
  266. }
  267. })
  268. // Define the additional component library for the used components which are not form fields.
  269. // Because of a typescript error, we need to cased the type: https://github.com/formkit/formkit/issues/274
  270. const additionalComponentLibrary = {
  271. FormLayout: markRaw(FormLayout) as unknown as ConcreteComponent,
  272. FormGroup: markRaw(FormGroup) as unknown as ConcreteComponent,
  273. }
  274. // Define the static schema, which will be filled with the real fields from the `schemaData`.
  275. const staticSchema = ref<FormKitSchemaNode[]>([])
  276. const fixedAndSkippedFields: string[] = []
  277. const schemaData = reactive<ReactiveFormSchemData>({
  278. fields: {},
  279. values,
  280. ...props.schemaData,
  281. })
  282. const internalFieldCamelizeName: Record<string, string> = {}
  283. const getInternalId = (item?: { id?: string; internalId?: number }) => {
  284. if (!item) return undefined
  285. if (item.internalId) return item.internalId
  286. if (!item.id) return undefined
  287. return parseGraphqlId(item.id).id
  288. }
  289. let initialEntityObjectAttributeMap: Record<string, FormFieldValue> = {}
  290. const setInitialEntityObjectAttributeMap = (
  291. initialEntityObject = props.initialEntityObject,
  292. ) => {
  293. if (isEmpty(initialEntityObject)) return
  294. const { objectAttributeValues } = initialEntityObject
  295. if (!objectAttributeValues) return
  296. // Reduce object attribute values to flat structure
  297. initialEntityObjectAttributeMap =
  298. objectAttributeValues.reduce((acc: Record<string, FormFieldValue>, cur) => {
  299. const { attribute } = cur
  300. if (!attribute || !attribute.name) return acc
  301. acc[attribute.name] = cur.value
  302. return acc
  303. }, {}) || {}
  304. }
  305. // Initialize the initial entity object attribute map during the setup in a static way.
  306. // It will maybe be updated later, when the resetForm is used with a different entity object.
  307. setInitialEntityObjectAttributeMap()
  308. const getInitialEntityObjectValue = (
  309. fieldName: string,
  310. initialEntityObject = props.initialEntityObject,
  311. ): FormFieldValue => {
  312. if (isEmpty(initialEntityObject)) return undefined
  313. let value: FormFieldValue
  314. if (relationFieldBelongsToObjectField[fieldName]) {
  315. const belongsToObject =
  316. initialEntityObject[relationFieldBelongsToObjectField[fieldName]]
  317. if (!belongsToObject) return undefined
  318. if ('edges' in belongsToObject) {
  319. value = edgesToArray(
  320. belongsToObject as { edges?: { node: { internalId: number } }[] },
  321. ).map((item) => getInternalId(item))
  322. } else {
  323. value = getInternalId(belongsToObject)
  324. }
  325. }
  326. if (!value) {
  327. const targetFieldName = internalFieldCamelizeName[fieldName] || fieldName
  328. value =
  329. targetFieldName in initialEntityObjectAttributeMap
  330. ? initialEntityObjectAttributeMap[targetFieldName]
  331. : initialEntityObject[targetFieldName]
  332. }
  333. return value
  334. }
  335. const getResetFormValues = (
  336. rootNode: FormKitNode,
  337. values: FormValues,
  338. object?: EntityObject,
  339. groupNode?: FormKitNode,
  340. resetDirty = true,
  341. ) => {
  342. const resetValues: FormValues = {}
  343. const dirtyNodes: FormKitNode[] = []
  344. const setResetFormValue = (
  345. name: string,
  346. value: FormFieldValue,
  347. parentName?: string,
  348. ) => {
  349. if (parentName) {
  350. resetValues[parentName] ||= {}
  351. ;(resetValues[parentName] as Record<string, FormFieldValue>)[name] = value
  352. return
  353. }
  354. resetValues[name] = value
  355. }
  356. Object.entries(schemaData.fields).forEach(([field, { props }]) => {
  357. const formElement = getNode(props.id || props.name)
  358. let parentName = ''
  359. if (formElement?.parent && formElement?.parent.name !== rootNode.name) {
  360. parentName = formElement.parent.name
  361. }
  362. // Do not use the parentName, when we are in group node reset context.
  363. const groupName = groupNode?.name
  364. if (groupName) {
  365. if (parentName !== groupName) return
  366. parentName = ''
  367. }
  368. if (!resetDirty && formElement?.context?.state.dirty) {
  369. dirtyNodes.push(formElement)
  370. setResetFormValue(field, formElement._value as FormFieldValue, parentName)
  371. return
  372. }
  373. if (field in values) {
  374. setResetFormValue(field, values[field], parentName)
  375. return
  376. }
  377. if (parentName && parentName in values) {
  378. const value = (values[parentName] as Record<string, FormFieldValue>)[
  379. field
  380. ]
  381. setResetFormValue(field, value, parentName)
  382. return
  383. }
  384. const objectValue = getInitialEntityObjectValue(field, object)
  385. if (objectValue !== undefined) {
  386. setResetFormValue(field, objectValue, parentName)
  387. }
  388. })
  389. return {
  390. dirtyNodes,
  391. resetValues,
  392. }
  393. }
  394. const resetForm = (
  395. values: FormValues = {},
  396. object: EntityObject | undefined = undefined,
  397. { resetDirty = true }: { resetDirty?: boolean } = {},
  398. groupNode: FormKitNode | undefined = undefined,
  399. ) => {
  400. if (!formNode.value) return
  401. const rootNode = formNode.value
  402. if (object) setInitialEntityObjectAttributeMap(object)
  403. const { dirtyNodes, resetValues } = getResetFormValues(
  404. rootNode,
  405. values,
  406. object,
  407. groupNode,
  408. resetDirty,
  409. )
  410. reset(
  411. groupNode || rootNode,
  412. Object.keys(resetValues).length ? resetValues : undefined,
  413. )
  414. // keep dirty nodes as dirty
  415. // TODO: check if we need to skip the formUpdater???
  416. dirtyNodes.forEach((node) => {
  417. node.input(node._value, false)
  418. })
  419. }
  420. defineExpose({
  421. formNode,
  422. formId,
  423. resetForm,
  424. })
  425. const localInitialValues: FormValues = { ...props.initialValues }
  426. const initializeFieldRelation = (
  427. fieldName: string,
  428. relation: FormSchemaField['relation'],
  429. belongsToObjectField?: string,
  430. ) => {
  431. if (relation) {
  432. relationFields.push({
  433. name: fieldName,
  434. relation: relation.type,
  435. filterIds: relation.filterIds,
  436. })
  437. }
  438. if (belongsToObjectField) {
  439. relationFieldBelongsToObjectField[fieldName] = belongsToObjectField
  440. }
  441. }
  442. const setInternalField = (fieldName: string, internal: boolean) => {
  443. if (!internal) return
  444. internalFieldCamelizeName[fieldName] = camelize(fieldName)
  445. }
  446. const updateSchemaLink = (
  447. specificProps: FormFieldAdditionalProps,
  448. fieldName: string,
  449. ) => {
  450. // native fields don't have link attribute, and we don't have a way to get rendered link from graphql
  451. const values = (props.initialEntityObject?.objectAttributeValues ||
  452. []) as ObjectAttributeValue[]
  453. const attribute = values.find(({ attribute }) => attribute.name === fieldName)
  454. if (attribute?.renderedLink) {
  455. specificProps.link = attribute.renderedLink
  456. }
  457. }
  458. const updateSchemaDataField = (
  459. field: FormSchemaField | SetRequired<Partial<FormSchemaField>, 'name'>,
  460. ) => {
  461. const {
  462. show,
  463. updateFields,
  464. relation,
  465. props: specificProps = {},
  466. ...fieldProps
  467. } = field
  468. const showField = show ?? schemaData.fields[field.name]?.show ?? true
  469. // Not needed in this context.
  470. delete fieldProps.if
  471. // Special handling for the disabled prop, so that the form can handle also
  472. // the disable state from outside.
  473. if ('disabled' in fieldProps && !fieldProps.disabled) {
  474. fieldProps.disabled = undefined
  475. }
  476. updateSchemaLink(fieldProps, field.name)
  477. if (schemaData.fields[field.name]) {
  478. schemaData.fields[field.name] = {
  479. show: showField,
  480. updateFields: updateFields || false,
  481. props: Object.assign(
  482. schemaData.fields[field.name].props,
  483. fieldProps,
  484. specificProps,
  485. ),
  486. }
  487. } else {
  488. initializeFieldRelation(
  489. field.name,
  490. relation,
  491. specificProps?.belongsToObjectField,
  492. )
  493. setInternalField(field.name, Boolean(fieldProps.internal))
  494. const combinedFieldProps = Object.assign(fieldProps, specificProps)
  495. // Select the correct initial value (at this time localInitialValues has not already the information
  496. // from the initial entity object, so we need to check it manually).
  497. combinedFieldProps.value =
  498. field.name in localInitialValues
  499. ? localInitialValues[field.name]
  500. : getInitialEntityObjectValue(field.name) ?? combinedFieldProps.value
  501. // Save current initial value for later usage, when not already exists.
  502. if (!(field.name in localInitialValues))
  503. localInitialValues[field.name] = combinedFieldProps.value
  504. schemaData.fields[field.name] = {
  505. show: showField,
  506. updateFields: updateFields || false,
  507. props: combinedFieldProps,
  508. }
  509. }
  510. }
  511. const updateChangedFields = (
  512. changedFields: Record<string, Partial<FormSchemaField>>,
  513. ) => {
  514. Object.keys(changedFields).forEach(async (fieldName) => {
  515. if (!schemaData.fields[fieldName]) return
  516. const { value, ...changedFieldProps } = changedFields[fieldName]
  517. const field: SetRequired<Partial<FormSchemaField>, 'name'> = {
  518. ...changedFieldProps,
  519. name: fieldName,
  520. }
  521. if (
  522. value !== undefined &&
  523. (!formKitInitialNodesSettled.value ||
  524. (!schemaData.fields[fieldName].show && changedFieldProps.show))
  525. ) {
  526. field.value = value
  527. }
  528. // When a field will be visible with the update call, we need to wait before on a settled form, before we
  529. // continue (so that we have all values present inside the form).
  530. // This situtation can happen, when the form is used very fast.
  531. if (
  532. formKitInitialNodesSettled.value &&
  533. !schemaData.fields[fieldName].show &&
  534. changedFieldProps.show &&
  535. !formNode.value?.isSettled
  536. ) {
  537. await formNode.value?.settled
  538. }
  539. updaterChangedFields.add(fieldName)
  540. updateSchemaDataField(field)
  541. if (!formKitInitialNodesSettled.value) return
  542. if (
  543. !('value' in field) &&
  544. value !== undefined &&
  545. value !== values.value[fieldName]
  546. ) {
  547. updaterChangedFields.add(fieldName)
  548. getNode(fieldName)?.input(value, false)
  549. }
  550. })
  551. nextTick(() => {
  552. updaterChangedFields.clear()
  553. formNode.value?.store.remove('formUpdaterProcessing')
  554. })
  555. }
  556. const formHandlerExecution: Record<
  557. FormHandlerExecution,
  558. FormHandlerFunction[]
  559. > = {
  560. [FormHandlerExecution.Initial]: [],
  561. [FormHandlerExecution.FieldChange]: [],
  562. }
  563. if (props.handlers) {
  564. props.handlers.forEach((handler) => {
  565. Object.values(FormHandlerExecution).forEach((execution) => {
  566. if (handler.execution.includes(execution)) {
  567. formHandlerExecution[execution].push(handler.callback)
  568. }
  569. })
  570. })
  571. }
  572. const executeFormHandler = (
  573. execution: FormHandlerExecution,
  574. currentValues: FormValues,
  575. changedField?: ChangedField,
  576. ) => {
  577. if (formHandlerExecution[execution].length === 0) return
  578. formHandlerExecution[execution].forEach((handler) => {
  579. handler(
  580. execution,
  581. formNode.value,
  582. currentValues,
  583. changeFields,
  584. updateSchemaDataField,
  585. schemaData,
  586. changedField,
  587. props.initialEntityObject,
  588. )
  589. })
  590. }
  591. const formUpdaterVariables = shallowRef<FormUpdaterQueryVariables>()
  592. let nextFormUpdaterVariables: Maybe<FormUpdaterQueryVariables>
  593. const executeFormUpdaterRefetch = () => {
  594. if (!nextFormUpdaterVariables) return
  595. formUpdaterVariables.value = nextFormUpdaterVariables
  596. // Reset the next variables so that it's not triggered a second time.
  597. nextFormUpdaterVariables = null
  598. }
  599. const handlesFormUpdater = (
  600. trigger: FormUpdaterTrigger,
  601. fieldName: string,
  602. newValue: FormFieldValue,
  603. oldValue: FormFieldValue,
  604. ) => {
  605. if (!props.formUpdaterId || !formUpdaterQueryHandler) return
  606. // We mark this as raw, because we want no deep reactivity on the form updater query variables.
  607. nextFormUpdaterVariables = markRaw({
  608. id: props.initialEntityObject?.id,
  609. formUpdaterId: props.formUpdaterId,
  610. data: {
  611. ...values.value,
  612. [fieldName]: newValue,
  613. },
  614. meta: {
  615. // We need a unique requestId, so that the query will always be executed on changes, also when the variables
  616. // are the same until the last request, because it could be that core workflow is setting a value back.
  617. requestId: getUuid(),
  618. formId,
  619. changedField: {
  620. name: fieldName,
  621. newValue,
  622. oldValue,
  623. },
  624. },
  625. relationFields,
  626. })
  627. formNode.value?.store.set(
  628. createMessage({
  629. blocking: true,
  630. key: 'formUpdaterProcessing',
  631. value: true,
  632. visible: false,
  633. }),
  634. )
  635. if (trigger !== 'blur') executeFormUpdaterRefetch()
  636. }
  637. const previousValues = new WeakMap<FormKitNode, FormFieldValue>()
  638. const changedInputValueHandling = (inputNode: FormKitNode) => {
  639. inputNode.on('commit', ({ payload: newValue, origin: node }) => {
  640. const oldValue = previousValues.get(node)
  641. if (isEqual(newValue, oldValue)) return
  642. if (!formKitInitialNodesSettled.value) {
  643. previousValues.set(node, cloneDeep(newValue))
  644. return
  645. }
  646. if (
  647. inputNode.props.triggerFormUpdater &&
  648. !updaterChangedFields.has(node.name)
  649. ) {
  650. handlesFormUpdater(
  651. inputNode.props.formUpdaterTrigger,
  652. node.name,
  653. newValue,
  654. oldValue,
  655. )
  656. }
  657. emit('changed', node.name, newValue, oldValue)
  658. executeFormHandler(FormHandlerExecution.FieldChange, values.value, {
  659. name: node.name,
  660. newValue,
  661. oldValue,
  662. })
  663. previousValues.set(node, cloneDeep(newValue))
  664. updaterChangedFields.delete(node.name)
  665. })
  666. inputNode.on('blur', async () => {
  667. if (inputNode.props.formUpdaterTrigger !== 'blur') return
  668. if (!formNode.value?.isSettled) await formNode.value?.settled
  669. if (nextFormUpdaterVariables) executeFormUpdaterRefetch()
  670. })
  671. inputNode.hook.message((payload: FormKitMessageProps, next) => {
  672. if (payload.key === 'submitted' && formUpdaterProcessing.value) {
  673. payload.value = false
  674. }
  675. return next(payload)
  676. })
  677. return false
  678. }
  679. const buildStaticSchema = () => {
  680. const { getFormFieldSchema, getFormFieldsFromScreen } =
  681. useObjectAttributeFormFields(fixedAndSkippedFields)
  682. const buildFormKitField = (
  683. field: FormSchemaField,
  684. ): FormKitSchemaComponent => {
  685. const fieldId = field.id || field.name
  686. return {
  687. $cmp: 'FormKit',
  688. if: field.if ? field.if : `$fields.${field.name}.show`,
  689. bind: `$fields.${field.name}.props`,
  690. props: {
  691. type: field.type,
  692. key: fieldId,
  693. name: field.name,
  694. id: fieldId,
  695. formId,
  696. plugins: [changedInputValueHandling],
  697. triggerFormUpdater: field.triggerFormUpdater ?? !!props.formUpdaterId,
  698. },
  699. }
  700. }
  701. const getLayoutType = (
  702. layoutNode: FormSchemaLayout,
  703. ): FormKitSchemaDOMNode | FormKitSchemaComponent => {
  704. let layoutField: FormKitSchemaDOMNode | FormKitSchemaComponent
  705. if ('component' in layoutNode) {
  706. layoutField = {
  707. $cmp: layoutNode.component,
  708. props: layoutNode.props,
  709. }
  710. } else {
  711. layoutField = {
  712. $el: layoutNode.element,
  713. attrs: layoutNode.attrs,
  714. }
  715. }
  716. if (layoutNode.if) {
  717. layoutField.if = layoutNode.if
  718. }
  719. return layoutField
  720. }
  721. type ResolveFormSchemaNode = Exclude<FormSchemaNode, string>
  722. type ResolveFormKitSchemaNode = Exclude<FormKitSchemaNode, string>
  723. const resolveSchemaNode = (
  724. node: ResolveFormSchemaNode,
  725. ): Maybe<ResolveFormKitSchemaNode | ResolveFormKitSchemaNode[]> => {
  726. if ('isLayout' in node && node.isLayout) {
  727. return getLayoutType(node)
  728. }
  729. if ('isGroupOrList' in node && node.isGroupOrList) {
  730. return {
  731. $cmp: 'FormKit',
  732. ...(node.if && { if: node.if }),
  733. props: {
  734. type: node.type,
  735. name: node.name,
  736. id: node.name,
  737. key: node.name,
  738. plugins: node.plugins,
  739. },
  740. }
  741. }
  742. if ('object' in node && getFormFieldSchema && getFormFieldsFromScreen) {
  743. if ('name' in node && node.name && !node.type) {
  744. const { screen, object, ...fieldNode } = node
  745. const resolvedField = getFormFieldSchema(fieldNode.name, object, screen)
  746. if (!resolvedField) return null
  747. node = {
  748. ...resolvedField,
  749. ...fieldNode,
  750. } as FormSchemaField
  751. } else if ('screen' in node && !('name' in node)) {
  752. const resolvedFields = getFormFieldsFromScreen(node.screen, node.object)
  753. const formKitFields: ResolveFormKitSchemaNode[] = []
  754. resolvedFields.forEach((screenField) => {
  755. updateSchemaDataField(screenField)
  756. formKitFields.push(buildFormKitField(screenField))
  757. })
  758. return formKitFields
  759. }
  760. }
  761. updateSchemaDataField(node as FormSchemaField)
  762. return buildFormKitField(node as FormSchemaField)
  763. }
  764. const resolveSchema = (schema: FormSchemaNode[] = props.schema) => {
  765. return schema.reduce((resolvedSchema: FormKitSchemaNode[], node) => {
  766. if (typeof node === 'string') {
  767. resolvedSchema.push(node)
  768. return resolvedSchema
  769. }
  770. const resolvedNode = resolveSchemaNode(node)
  771. if (!resolvedNode) return resolvedSchema
  772. if ('children' in node) {
  773. const childrens = Array.isArray(node.children)
  774. ? [...resolveSchema(node.children)]
  775. : node.children
  776. resolvedSchema.push({
  777. ...(resolvedNode as Exclude<FormKitSchemaNode, string>),
  778. children: childrens,
  779. })
  780. return resolvedSchema
  781. }
  782. if (Array.isArray(resolvedNode)) {
  783. resolvedSchema.push(...resolvedNode)
  784. } else {
  785. resolvedSchema.push(resolvedNode)
  786. }
  787. return resolvedSchema
  788. }, [])
  789. }
  790. staticSchema.value = resolveSchema()
  791. }
  792. watchOnce(formKitInitialNodesSettled, () => {
  793. watch(
  794. changeFields,
  795. (newValue) => {
  796. updateChangedFields(newValue)
  797. },
  798. {
  799. deep: true,
  800. },
  801. )
  802. })
  803. watch(
  804. () => props.schemaData,
  805. () => Object.assign(schemaData, props.schemaData),
  806. {
  807. deep: true,
  808. },
  809. )
  810. const setFormSchemaInitialized = () => {
  811. if (!formSchemaInitialized.value) {
  812. formSchemaInitialized.value = true
  813. }
  814. }
  815. const initializeFormSchema = () => {
  816. buildStaticSchema()
  817. if (props.formUpdaterId) {
  818. formUpdaterVariables.value = markRaw({
  819. id: props.initialEntityObject?.id,
  820. formUpdaterId: props.formUpdaterId,
  821. data: localInitialValues,
  822. meta: {
  823. initial: true,
  824. formId,
  825. },
  826. relationFields,
  827. })
  828. formUpdaterQueryHandler = new QueryHandler(
  829. useFormUpdaterQuery(
  830. formUpdaterVariables as Ref<FormUpdaterQueryVariables>,
  831. {
  832. fetchPolicy: 'no-cache',
  833. },
  834. ),
  835. )
  836. formUpdaterQueryHandler.onResult((queryResult) => {
  837. // Execute the form handler function so that they can manipulate the form updater result.
  838. if (!formSchemaInitialized.value) {
  839. executeFormHandler(FormHandlerExecution.Initial, localInitialValues)
  840. }
  841. if (queryResult?.data.formUpdater) {
  842. updateChangedFields(
  843. changeFields.value
  844. ? merge(queryResult.data.formUpdater, changeFields.value)
  845. : queryResult.data.formUpdater,
  846. )
  847. }
  848. setFormSchemaInitialized()
  849. })
  850. } else {
  851. executeFormHandler(FormHandlerExecution.Initial, localInitialValues)
  852. if (changeFields.value) updateChangedFields(changeFields.value)
  853. setFormSchemaInitialized()
  854. }
  855. }
  856. // TODO: maybe we should react on schema changes and rebuild the static schema with a new form-id and re-rendering of
  857. // the complete form (= use the formId as the key for the whole form to trigger the re-rendering of the component...)
  858. if (props.schema) {
  859. showInitialLoadingAnimation.value = true
  860. if (props.useObjectAttributes) {
  861. // TODO: rebuild schema, when object attributes
  862. // was changed from outside(not such important,
  863. // because we have currently the reload solution like in the desktop view).
  864. if (props.objectAttributeSkippedFields) {
  865. fixedAndSkippedFields.push(...props.objectAttributeSkippedFields)
  866. }
  867. const objectAttributeObjects: EnumObjectManagerObjects[] = []
  868. const addObjectAttributeToObjects = (object: EnumObjectManagerObjects) => {
  869. if (objectAttributeObjects.includes(object)) return
  870. objectAttributeObjects.push(object)
  871. }
  872. const detectObjectAttributeObjects = (
  873. schema: FormSchemaNode[] = props.schema,
  874. ) => {
  875. schema.forEach((item) => {
  876. if (typeof item === 'string') return
  877. if ('object' in item) {
  878. if ('name' in item && item.name && !item.type) {
  879. fixedAndSkippedFields.push(item.name)
  880. }
  881. addObjectAttributeToObjects(item.object)
  882. }
  883. if ('children' in item && Array.isArray(item.children)) {
  884. detectObjectAttributeObjects(item.children)
  885. }
  886. })
  887. }
  888. detectObjectAttributeObjects()
  889. // We need only to fetch object attributes, when there are used in the given schema.
  890. if (objectAttributeObjects.length > 0) {
  891. const { objectAttributesLoading } = useObjectAttributeLoadFormFields(
  892. objectAttributeObjects,
  893. )
  894. const unwatchTriggerFormInitialize = watch(
  895. objectAttributesLoading,
  896. (loading) => {
  897. if (!loading) {
  898. nextTick(() => unwatchTriggerFormInitialize())
  899. initializeFormSchema()
  900. }
  901. },
  902. { immediate: true },
  903. )
  904. } else {
  905. initializeFormSchema()
  906. }
  907. } else {
  908. initializeFormSchema()
  909. }
  910. }
  911. </script>
  912. <script lang="ts">
  913. export default {
  914. inheritAttrs: false,
  915. }
  916. </script>
  917. <template>
  918. <div
  919. v-if="debouncedShowInitialLoadingAnimation"
  920. class="flex items-center justify-center"
  921. >
  922. <CommonIcon name="mobile-loading" animation="spin" />
  923. </div>
  924. <FormKit
  925. v-if="
  926. hasSchema &&
  927. ((formSchemaInitialized && Object.keys(schemaData.fields).length > 0) ||
  928. $slots.default)
  929. "
  930. v-bind="$attrs"
  931. :id="id"
  932. type="form"
  933. novalidate
  934. :config="formConfig"
  935. :form-class="localClass"
  936. :actions="false"
  937. :incomplete-message="false"
  938. :plugins="localFormKitPlugins"
  939. :sections-schema="formKitSectionsSchema"
  940. :disabled="disabled"
  941. @node="setFormNode"
  942. @submit="onSubmit"
  943. @submit-raw="onSubmitRaw"
  944. >
  945. <slot name="before-fields" />
  946. <slot
  947. name="default"
  948. :schema="staticSchema"
  949. :data="schemaData"
  950. :library="additionalComponentLibrary"
  951. >
  952. <div
  953. v-show="
  954. formKitInitialNodesSettled && !debouncedShowInitialLoadingAnimation
  955. "
  956. ref="formElement"
  957. >
  958. <FormKitSchema
  959. :schema="staticSchema"
  960. :data="schemaData"
  961. :library="additionalComponentLibrary"
  962. />
  963. </div>
  964. </slot>
  965. <slot name="after-fields" />
  966. </FormKit>
  967. </template>