Form.vue 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281
  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. return {
  760. $cmp: 'FormKit',
  761. if: field.if ? field.if : `$fields.${field.name}.show`,
  762. bind: `$fields.${field.name}.props`,
  763. props: {
  764. type: field.type,
  765. key: fieldId,
  766. name: field.name,
  767. id: fieldId,
  768. formId,
  769. plugins: [changedInputValueHandling],
  770. triggerFormUpdater: field.triggerFormUpdater ?? !!props.formUpdaterId,
  771. },
  772. }
  773. }
  774. const getLayoutType = (
  775. layoutNode: FormSchemaLayout,
  776. ): FormKitSchemaDOMNode | FormKitSchemaComponent => {
  777. let layoutField: FormKitSchemaDOMNode | FormKitSchemaComponent
  778. if ('component' in layoutNode) {
  779. layoutField = {
  780. $cmp: layoutNode.component,
  781. ...(layoutNode.if && { if: layoutNode.if }),
  782. props: layoutNode.props,
  783. }
  784. } else {
  785. layoutField = {
  786. $el: layoutNode.element,
  787. ...(layoutNode.if && { if: layoutNode.if }),
  788. attrs: layoutNode.attrs,
  789. }
  790. }
  791. if (layoutNode.if) {
  792. layoutField.if = layoutNode.if
  793. }
  794. return layoutField
  795. }
  796. type ResolveFormSchemaNode = Exclude<FormSchemaNode, string>
  797. type ResolveFormKitSchemaNode = Exclude<FormKitSchemaNode, string>
  798. const resolveSchemaNode = (
  799. node: ResolveFormSchemaNode,
  800. ): Maybe<ResolveFormKitSchemaNode | ResolveFormKitSchemaNode[]> => {
  801. if ('isLayout' in node && node.isLayout) {
  802. return getLayoutType(node)
  803. }
  804. if ('isGroupOrList' in node && node.isGroupOrList) {
  805. return {
  806. $cmp: 'FormKit',
  807. ...(node.if && { if: node.if }),
  808. props: {
  809. type: node.type,
  810. name: node.name,
  811. id: node.name,
  812. key: node.name,
  813. plugins: node.plugins,
  814. },
  815. }
  816. }
  817. if ('object' in node && getFormFieldSchema && getFormFieldsFromScreen) {
  818. if ('name' in node && node.name && !node.type) {
  819. const { screen, object, ...fieldNode } = node
  820. const resolvedField = getFormFieldSchema(fieldNode.name, object, screen)
  821. if (!resolvedField) return null
  822. node = {
  823. ...resolvedField,
  824. ...fieldNode,
  825. } as FormSchemaField
  826. } else if ('screen' in node && !('name' in node)) {
  827. const resolvedFields = getFormFieldsFromScreen(node.screen, node.object)
  828. const formKitFields: ResolveFormKitSchemaNode[] = []
  829. resolvedFields.forEach((screenField) => {
  830. updateSchemaDataField(screenField)
  831. formKitFields.push(buildFormKitField(screenField))
  832. })
  833. return formKitFields
  834. }
  835. }
  836. updateSchemaDataField(node as FormSchemaField)
  837. return buildFormKitField(node as FormSchemaField)
  838. }
  839. const resolveSchema = (schema: FormSchemaNode[] = props.schema) => {
  840. return schema.reduce((resolvedSchema: FormKitSchemaNode[], node) => {
  841. if (typeof node === 'string') {
  842. resolvedSchema.push(node)
  843. return resolvedSchema
  844. }
  845. const resolvedNode = resolveSchemaNode(node)
  846. if (!resolvedNode) return resolvedSchema
  847. if ('children' in node) {
  848. const childrens = Array.isArray(node.children)
  849. ? [...resolveSchema(node.children)]
  850. : node.children
  851. resolvedSchema.push({
  852. ...(resolvedNode as Exclude<FormKitSchemaNode, string>),
  853. children: childrens,
  854. })
  855. return resolvedSchema
  856. }
  857. if (Array.isArray(resolvedNode)) {
  858. resolvedSchema.push(...resolvedNode)
  859. } else {
  860. resolvedSchema.push(resolvedNode)
  861. }
  862. return resolvedSchema
  863. }, [])
  864. }
  865. staticSchema.value = resolveSchema()
  866. }
  867. watchOnce(formKitInitialNodesSettled, () => {
  868. watch(
  869. changeFields,
  870. (newValue) => {
  871. updateChangedFields(newValue)
  872. },
  873. {
  874. deep: true,
  875. },
  876. )
  877. })
  878. watch(
  879. () => props.schemaData,
  880. () => Object.assign(schemaData, props.schemaData),
  881. {
  882. deep: true,
  883. },
  884. )
  885. const setFormSchemaInitialized = () => {
  886. if (!formSchemaInitialized.value) {
  887. formSchemaInitialized.value = true
  888. }
  889. }
  890. const initializeFormSchema = () => {
  891. buildStaticSchema()
  892. if (props.formUpdaterId) {
  893. formUpdaterVariables.value = markRaw({
  894. id: props.initialEntityObject?.id,
  895. formUpdaterId: props.formUpdaterId,
  896. data: localInitialValues,
  897. meta: {
  898. initial: true,
  899. formId,
  900. },
  901. relationFields,
  902. })
  903. formUpdaterQueryHandler = new QueryHandler(
  904. useFormUpdaterQuery(
  905. formUpdaterVariables as Ref<FormUpdaterQueryVariables>,
  906. {
  907. fetchPolicy: 'no-cache',
  908. },
  909. ),
  910. )
  911. formUpdaterQueryHandler.onResult((queryResult) => {
  912. // Execute the form handler function so that they can manipulate the form updater result.
  913. if (!formSchemaInitialized.value) {
  914. executeFormHandler(FormHandlerExecution.Initial, localInitialValues)
  915. }
  916. if (queryResult?.data?.formUpdater) {
  917. updateChangedFields(
  918. changeFields.value
  919. ? merge(queryResult.data.formUpdater, changeFields.value)
  920. : queryResult.data.formUpdater,
  921. )
  922. }
  923. setFormSchemaInitialized()
  924. })
  925. } else {
  926. executeFormHandler(FormHandlerExecution.Initial, localInitialValues)
  927. if (changeFields.value) updateChangedFields(changeFields.value)
  928. setFormSchemaInitialized()
  929. }
  930. }
  931. // TODO: maybe we should react on schema changes and rebuild the static schema with a new form-id and re-rendering of
  932. // the complete form (= use the formId as the key for the whole form to trigger the re-rendering of the component...)
  933. if (props.schema) {
  934. showInitialLoadingAnimation.value = true
  935. if (props.useObjectAttributes) {
  936. // TODO: rebuild schema, when object attributes
  937. // was changed from outside(not such important,
  938. // because we have currently the reload solution like in the desktop view).
  939. if (props.objectAttributeSkippedFields) {
  940. fixedAndSkippedFields.push(...props.objectAttributeSkippedFields)
  941. }
  942. const objectAttributeObjects: EnumObjectManagerObjects[] = []
  943. const addObjectAttributeToObjects = (object: EnumObjectManagerObjects) => {
  944. if (objectAttributeObjects.includes(object)) return
  945. objectAttributeObjects.push(object)
  946. }
  947. const detectObjectAttributeObjects = (
  948. schema: FormSchemaNode[] = props.schema,
  949. ) => {
  950. schema.forEach((item) => {
  951. if (typeof item === 'string') return
  952. if ('object' in item) {
  953. if ('name' in item && item.name && !item.type) {
  954. fixedAndSkippedFields.push(item.name)
  955. }
  956. addObjectAttributeToObjects(item.object)
  957. }
  958. if ('children' in item && Array.isArray(item.children)) {
  959. detectObjectAttributeObjects(item.children)
  960. }
  961. })
  962. }
  963. detectObjectAttributeObjects()
  964. // We need only to fetch object attributes, when there are used in the given schema.
  965. if (objectAttributeObjects.length > 0) {
  966. const { objectAttributesLoading } = useObjectAttributeLoadFormFields(
  967. objectAttributeObjects,
  968. )
  969. const unwatchTriggerFormInitialize = watch(
  970. objectAttributesLoading,
  971. (loading) => {
  972. if (!loading) {
  973. nextTick(() => unwatchTriggerFormInitialize())
  974. initializeFormSchema()
  975. }
  976. },
  977. { immediate: true },
  978. )
  979. } else {
  980. initializeFormSchema()
  981. }
  982. } else {
  983. initializeFormSchema()
  984. }
  985. }
  986. const classMap = getFormClasses()
  987. defineExpose({
  988. formNode,
  989. formId,
  990. values,
  991. updateChangedFields,
  992. updateSchemaDataField,
  993. getNodeByName,
  994. findNodeByName,
  995. resetForm,
  996. })
  997. </script>
  998. <script lang="ts">
  999. export default {
  1000. inheritAttrs: false,
  1001. }
  1002. </script>
  1003. <template>
  1004. <div
  1005. v-if="debouncedShowInitialLoadingAnimation"
  1006. class="flex items-center justify-center"
  1007. >
  1008. <CommonIcon :class="classMap.loading" name="loading" animation="spin" />
  1009. </div>
  1010. <FormKit
  1011. v-if="
  1012. hasSchema &&
  1013. ((formSchemaInitialized && Object.keys(schemaData.fields).length > 0) ||
  1014. $slots.default)
  1015. "
  1016. v-bind="$attrs"
  1017. :id="id"
  1018. type="form"
  1019. novalidate
  1020. :config="formConfig"
  1021. :form-class="localClass"
  1022. :actions="false"
  1023. :incomplete-message="false"
  1024. :plugins="localFormKitPlugins"
  1025. :sections-schema="formKitSectionsSchema"
  1026. :disabled="disabled"
  1027. @node="setFormNode"
  1028. @submit="onSubmit"
  1029. @submit-raw="onSubmitRaw"
  1030. >
  1031. <FormKitMessages
  1032. :sections-schema="{
  1033. messages: {
  1034. $el: 'div',
  1035. },
  1036. message: {
  1037. $el: undefined,
  1038. $cmp: 'CommonAlert',
  1039. props: {
  1040. id: `$id + '-' + $message.key`,
  1041. key: '$message.key',
  1042. variant: {
  1043. if: '$message.type == error || $message.type == validation',
  1044. then: 'danger',
  1045. else: '$message.type',
  1046. },
  1047. },
  1048. slots: {
  1049. default: '$message.value',
  1050. },
  1051. },
  1052. }"
  1053. />
  1054. <slot name="before-fields" />
  1055. <slot
  1056. name="default"
  1057. :schema="staticSchema"
  1058. :data="schemaData"
  1059. :library="additionalComponentLibrary"
  1060. >
  1061. <div
  1062. v-show="
  1063. formKitInitialNodesSettled && !debouncedShowInitialLoadingAnimation
  1064. "
  1065. ref="formElement"
  1066. :class="formClass"
  1067. >
  1068. <FormKitSchema
  1069. :schema="staticSchema"
  1070. :data="schemaData"
  1071. :library="additionalComponentLibrary"
  1072. />
  1073. </div>
  1074. </slot>
  1075. <slot name="after-fields" />
  1076. </FormKit>
  1077. </template>