From 92300dda81b20b4bec1fb4f7e79921838291e19d Mon Sep 17 00:00:00 2001 From: Florian Liebe Date: Wed, 6 Dec 2023 09:17:48 +0100 Subject: [PATCH] Maintenance: Refactor public interface for reactive data in forms. --- .../useTicketDuplicateDetectionHandler.ts | 17 +++--- .../ticket/composable/useTicketEditForm.ts | 11 ++-- app/frontend/shared/components/Form/Form.vue | 22 ++++--- app/frontend/shared/components/Form/types.ts | 28 ++++++--- .../shared/composables/useTicketSignature.ts | 10 ++-- .../useTicketFormOrganizationHandler.ts | 12 ++-- doc/developer_manual/index.md | 1 + .../standards/how-to-use-forms.md | 59 +++++++++++++++++++ 8 files changed, 114 insertions(+), 46 deletions(-) create mode 100644 doc/developer_manual/standards/how-to-use-forms.md diff --git a/app/frontend/apps/mobile/pages/ticket/composable/useTicketDuplicateDetectionHandler.ts b/app/frontend/apps/mobile/pages/ticket/composable/useTicketDuplicateDetectionHandler.ts index bde7a6d6c6b0..7678034480ba 100644 --- a/app/frontend/apps/mobile/pages/ticket/composable/useTicketDuplicateDetectionHandler.ts +++ b/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 { diff --git a/app/frontend/apps/mobile/pages/ticket/composable/useTicketEditForm.ts b/app/frontend/apps/mobile/pages/ticket/composable/useTicketEditForm.ts index f88e2d3a5647..dc29a7ab8a12 100644 --- a/app/frontend/apps/mobile/pages/ticket/composable/useTicketEditForm.ts +++ b/app/frontend/apps/mobile/pages/ticket/composable/useTicketEditForm.ts @@ -249,13 +249,12 @@ export const useTicketEditForm = (ticket: Ref) => { 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 || diff --git a/app/frontend/shared/components/Form/Form.vue b/app/frontend/shared/components/Form/Form.vue index d65c7bde0739..214a5a5159d1 100644 --- a/app/frontend/shared/components/Form/Form.vue +++ b/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 handlers?: FormHandler[] changeFields?: Record> + 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 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, + }, ) }) } diff --git a/app/frontend/shared/components/Form/types.ts b/app/frontend/shared/components/Form/types.ts index 0fc34e2b8585..e8ddedbb7574 100644 --- a/app/frontend/shared/components/Form/types.ts +++ b/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>>, +export interface FormHandlerFunctionData { + formNode: FormKitNode | undefined + values: FormValues + changedField?: ChangedField + initialEntityObject?: ObjectLike +} + +export interface FormHandlerFunctionReactivity { + changeFields: Ref>> + 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, 'name'>, - ) => void, - schemaData: ReactiveFormSchemData, - changedField?: ChangedField, - initialEntityObject?: ObjectLike, + ) => void +} + +export type FormHandlerFunction = ( + execution: FormHandlerExecution, + reactivity: FormHandlerFunctionReactivity, + data: FormHandlerFunctionData, ) => void export interface FormHandler { diff --git a/app/frontend/shared/composables/useTicketSignature.ts b/app/frontend/shared/composables/useTicketSignature.ts index d8f9a0cd38f2..9ccbd0ab3de0 100644 --- a/app/frontend/shared/composables/useTicketSignature.ts +++ b/app/frontend/shared/composables/useTicketSignature.ts @@ -51,13 +51,11 @@ export const useTicketSignature = (ticket?: Ref) => { 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' diff --git a/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts b/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts index 037b35e8ac9b..3c90698cd900 100644 --- a/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts +++ b/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() diff --git a/doc/developer_manual/index.md b/doc/developer_manual/index.md index 4d412fec5c44..733666810855 100644 --- a/doc/developer_manual/index.md +++ b/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 diff --git a/doc/developer_manual/standards/how-to-use-forms.md b/doc/developer_manual/standards/how-to-use-forms.md new file mode 100644 index 000000000000..9797b35bffb9 --- /dev/null +++ b/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).