Form.vue 36 KB

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