Form.vue 36 KB

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