Form.vue 41 KB

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