Form.vue 32 KB

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