Browse Source

Maintenance: Refactor public interface for reactive data in forms.

Florian Liebe 1 year ago
parent
commit
92300dda81

+ 8 - 9
app/frontend/apps/mobile/pages/ticket/composable/useTicketDuplicateDetectionHandler.ts

@@ -42,21 +42,20 @@ export const useTicketDuplicateDetectionHandler = (
 
   const handleTicketDuplicateDetection: FormHandlerFunction = async (
     execution,
-    formNode,
-    values,
-    changeFields,
-    updateSchemaDataField,
-    schemaData,
-    changedField,
+    reactivity,
+    data,
   ) => {
+    const { changedField } = data
+    const { schemaData } = reactivity
+
     if (!executeHandler(execution, schemaData, changedField)) return
 
-    const data =
+    const newFieldData =
       changedField?.newValue as unknown as TicketDuplicateDetectionPayload
 
-    if (!data?.count) return
+    if (!newFieldData?.count) return
 
-    showTicketDuplicateDetectionDialog(data)
+    showTicketDuplicateDetectionDialog(newFieldData)
   }
 
   return {

+ 5 - 6
app/frontend/apps/mobile/pages/ticket/composable/useTicketEditForm.ts

@@ -249,13 +249,12 @@ export const useTicketEditForm = (ticket: Ref<TicketById | undefined>) => {
 
     const handleArticleType: FormHandlerFunction = (
       execution,
-      formNode,
-      values,
-      changeFields,
-      updateSchemaDataField,
-      schemaData,
-      changedField,
+      reactivity,
+      data,
     ) => {
+      const { formNode, changedField } = data
+      const { schemaData } = reactivity
+
       if (
         !executeHandler(execution, schemaData, changedField) ||
         !ticket.value ||

+ 13 - 9
app/frontend/shared/components/Form/Form.vue

@@ -74,13 +74,13 @@ import FormGroup from './FormGroup.vue'
 export interface Props {
   id?: string
   schema?: FormSchemaNode[]
-  formUpdaterId?: EnumFormUpdaterId
+  schemaData?: Except<ReactiveFormSchemData, 'fields'>
   handlers?: FormHandler[]
   changeFields?: Record<string, Partial<FormSchemaField>>
+  formUpdaterId?: EnumFormUpdaterId
   // Maybe in the future this is no longer needed, when FormKit supports group
   // without value grouping below group name (https://github.com/formkit/formkit/issues/461).
   flattenFormGroups?: string[]
-  schemaData?: Except<ReactiveFormSchemData, 'fields'>
   formKitPlugins?: FormKitPlugin[]
   formKitSectionsSchema?: Record<
     string,
@@ -774,13 +774,17 @@ const executeFormHandler = (
   formHandlerExecution[execution].forEach((handler) => {
     handler(
       execution,
-      formNode.value,
-      currentValues,
-      changeFields,
-      updateSchemaDataField,
-      schemaData,
-      changedField,
-      props.initialEntityObject,
+      {
+        changeFields,
+        updateSchemaDataField,
+        schemaData,
+      },
+      {
+        formNode: formNode.value,
+        values: currentValues,
+        changedField,
+        initialEntityObject: props.initialEntityObject,
+      },
     )
   })
 }

+ 19 - 9
app/frontend/shared/components/Form/types.ts

@@ -196,17 +196,27 @@ export enum FormHandlerExecution {
   FieldChange = 'fieldChange',
 }
 
-export type FormHandlerFunction = (
-  execution: FormHandlerExecution,
-  formNode: FormKitNode | undefined,
-  values: FormValues,
-  changeFields: Ref<Record<string, Partial<FormSchemaField>>>,
+export interface FormHandlerFunctionData {
+  formNode: FormKitNode | undefined
+  values: FormValues
+  changedField?: ChangedField
+  initialEntityObject?: ObjectLike
+}
+
+export interface FormHandlerFunctionReactivity {
+  changeFields: Ref<Record<string, Partial<FormSchemaField>>>
+  schemaData: ReactiveFormSchemData
+  // This can be used to update the current schema data, but without remembering it inside
+  // the changeFields and schemaData objects (which means it's persistent).
   updateSchemaDataField: (
     field: FormSchemaField | SetRequired<Partial<FormSchemaField>, 'name'>,
-  ) => void,
-  schemaData: ReactiveFormSchemData,
-  changedField?: ChangedField,
-  initialEntityObject?: ObjectLike,
+  ) => void
+}
+
+export type FormHandlerFunction = (
+  execution: FormHandlerExecution,
+  reactivity: FormHandlerFunctionReactivity,
+  data: FormHandlerFunctionData,
 ) => void
 
 export interface FormHandler {

+ 4 - 6
app/frontend/shared/composables/useTicketSignature.ts

@@ -51,13 +51,11 @@ export const useTicketSignature = (ticket?: Ref<TicketById | undefined>) => {
   const signatureHandling = (editorName: string): FormHandler => {
     const handleSignature: FormHandlerFunction = (
       execution,
-      formNode,
-      values,
-      changeFields,
-      updateSchemaDataField,
-      schemaData,
-      changedField,
+      reactivity,
+      data,
     ) => {
+      const { formNode, values, changedField } = data
+
       if (
         changedField?.name !== 'group_id' &&
         changedField?.name !== 'articleSenderType'

+ 5 - 7
app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts

@@ -33,15 +33,13 @@ export const useTicketFormOganizationHandler = (): FormHandler => {
 
   const handleOrganizationField: FormHandlerFunction = (
     execution,
-    formNode,
-    values,
-    changeFields,
-    updateSchemaDataField,
-    schemaData,
-    changedField,
-    initialEntityObject,
+    reactivity,
+    data,
     // eslint-disable-next-line sonarjs/cognitive-complexity
   ) => {
+    const { formNode, values, initialEntityObject, changedField } = data
+    const { schemaData, changeFields, updateSchemaDataField } = reactivity
+
     if (!executeHandler(execution, schemaData, changedField)) return
 
     const session = useSessionStore()

+ 1 - 0
doc/developer_manual/index.md

@@ -16,6 +16,7 @@ Welcome to the developer docs of Zammad. 👋 This is a work in progress, and yo
 - [How to add an SVG Icon](standards/how-to-add-an-svg-icon.md)
 - [How to handle localization & translations](standards/how-to-handle-localization.md)
 - [How to rebuild the chat](standards/how-to-rebuild-the-chat.md)
+- [How to use forms](standards/how-to-use-forms.md)
 
 # Cookbook / Recipes
 

+ 59 - 0
doc/developer_manual/standards/how-to-use-forms.md

@@ -0,0 +1,59 @@
+# How to Use Forms
+
+## Basics
+
+Forms in Zammad are based on [FormKit](https://formkit.com/) and the documentation is referenced in the following paragraphs.
+
+They are defined by the `schema`. The schema data describes the form containing all needed form fields, e.g. [the ticket creation screen](https://github.com/zammad/zammad/blob/develop/app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue#L121). For more information, please see the [Formkit schema essentials description](https://formkit.com/essentials/schema).
+
+## Usage of Reactivity
+
+The forms provide reactivity to modify the form and form fields on events, e.g. user input or data manipulation.
+
+### Schema Data
+
+In addition to the static schema, the `Form` component can also include a `schemaData` prop. Values from the data object and properties can then be referenced, and your schema will maintain the reactivity of the original data object.
+
+To reference a value from the data object, you simply use `$` followed by the property name from the data object. References can be used in the schema `attrs`, `props`, `conditionals`, and as children. Please have a look at the [FormKit references page](https://formkit.com/essentials/schema#references).
+
+In our implementation, the current form values are always available as `$values`.
+
+Example:
+
+```ts
+const schemaData = reactive({
+  securityIntegration: false,
+})
+```
+
+Example (excerpt of static schema)
+
+```ts
+{
+  if: '$securityIntegration === true && $values.articleSenderType === "email-out"',
+  name: 'security',
+  label: __('Security'),
+  type: 'security',
+}
+```
+
+### Change Fields
+
+The `changeFields` is a reactive extension for the form implementation.
+
+This is our preferred way of changing the state of fields after a user interacts with the form. This should be manipulated with the `changed` event of the form.
+
+A simple use case is to mark some fields as mandatory after a user selects the value `Support Request` in the field `Category`.
+
+### Handlers
+
+This is the most powerful way to influence the behavior of the current form's reactivity.
+
+If you need to share code between multiple forms that relate to reactivity, you have to use form handlers.
+
+The form handler supports two execution types:
+
+- `Initial`
+- `FieldChange`
+
+For a working example of a handler, please have a look at the [ticket signature code](https://github.com/zammad/zammad/blob/develop/app/frontend/shared/composables/useTicketSignature.ts).