Browse Source

Feature: Mobile - Added form improvements for multi step usage.

Dominik Klein 2 years ago
parent
commit
4952d493e1

+ 17 - 18
app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue

@@ -9,26 +9,20 @@ import {
   EnumObjectManagerObjects,
 } from '@shared/graphql/types'
 import { ref } from 'vue'
-// import { computed } from 'vue'
-
-// fixed fields? (e.g. title)
-// skip fields for mobile? (e.g. user_permission for user create/edit)
-// get only one screen for placement
-
-// title fixed
-// create_top
-// some article fields
-
-// create_middle
-
-// create_bottom
 
 const additionalFormSchema = [
   {
-    name: 'title',
-    required: true,
-    object: EnumObjectManagerObjects.Ticket,
-    screen: 'create_top',
+    type: 'group',
+    name: 'step1',
+    isGroupOrList: true,
+    children: [
+      {
+        name: 'title',
+        required: true,
+        object: EnumObjectManagerObjects.Ticket,
+        screen: 'create_top',
+      },
+    ],
   },
   { screen: 'create_top', object: EnumObjectManagerObjects.Ticket },
   {
@@ -51,7 +45,11 @@ const submit = (data: unknown) => {
   console.log('VALUES', data)
 }
 
-const changeHiddenFields = ref<Record<string, Partial<FormSchemaField>>>({})
+const changeHiddenFields = ref<Record<string, Partial<FormSchemaField>>>({
+  title: {
+    required: true,
+  },
+})
 
 const changeHidden = () => {
   changeHiddenFields.value.type = {
@@ -71,6 +69,7 @@ const changeHidden = () => {
       class="text-left"
       :schema="additionalFormSchema"
       :change-fields="changeHiddenFields"
+      :multi-step-form-groups="['step1']"
       :form-updater-id="EnumFormUpdaterId.FormUpdaterUpdaterTicketCreate"
       use-object-attributes
       @submit="submit"

+ 24 - 2
app/frontend/shared/components/Form/Form.vue

@@ -72,6 +72,9 @@ export interface Props {
   schema?: FormSchemaNode[]
   formUpdaterId?: EnumFormUpdaterId
   changeFields?: Record<string, Partial<FormSchemaField>>
+  // 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).
+  multiStepFormGroups?: string[]
   formKitPlugins?: FormKitPlugin[]
   formKitSectionsSchema?: Record<
     string,
@@ -198,12 +201,29 @@ defineExpose({
   formNode,
 })
 
+// Build the flat value, when multi step form groups are used.
+const getFlatValues = (values: FormValues, multiStepFormGroups: string[]) => {
+  const flatValues = {
+    ...values,
+  }
+
+  multiStepFormGroups.forEach((stepFormGroup) => {
+    Object.assign(flatValues, flatValues[stepFormGroup])
+    delete flatValues[stepFormGroup]
+  })
+
+  return flatValues
+}
+
 // Use the node context value, instead of the v-model, because of performance reason.
 const values = computed<FormValues>(() => {
   if (!formNodeContext.value) {
     return {}
   }
-  return formNodeContext.value.value
+
+  if (!props.multiStepFormGroups) return formNodeContext.value.value
+
+  return getFlatValues(formNodeContext.value.value, props.multiStepFormGroups)
 })
 
 const relationFields: FormUpdaterRelationField[] = []
@@ -225,7 +245,9 @@ const onSubmit = (values: FormData): Promise<void> | void => {
   if (!props.onSubmit) return undefined
 
   const emitValues = {
-    ...values,
+    ...(props.multiStepFormGroups
+      ? getFlatValues(values, props.multiStepFormGroups)
+      : values),
     formId,
   }
 

+ 82 - 6
app/frontend/shared/components/Form/__tests__/Form.spec.ts

@@ -1,17 +1,18 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
-import Form from '@shared/components/Form/Form.vue'
-import type { Props } from '@shared/components/Form/Form.vue'
-import UserError from '@shared/errors/UserError'
-import type { FormValues } from '@shared/components/Form'
+import { onMounted, ref } from 'vue'
 import type { FormKitNode } from '@formkit/core'
+import { getNode } from '@formkit/core'
 import { within } from '@testing-library/vue'
 import type { ExtendedMountingOptions } from '@tests/support/components'
 import { renderComponent } from '@tests/support/components'
+import { mockGraphQLApi } from '@tests/support/mock-graphql-api'
 import { waitForNextTick, waitUntil } from '@tests/support/utils'
-import { onMounted, ref } from 'vue'
+import Form from '@shared/components/Form/Form.vue'
+import type { Props } from '@shared/components/Form/Form.vue'
+import UserError from '@shared/errors/UserError'
+import type { FormValues } from '@shared/components/Form'
 import { EnumObjectManagerObjects } from '@shared/graphql/types'
-import { mockGraphQLApi } from '@tests/support/mock-graphql-api'
 import { ObjectManagerFrontendAttributesDocument } from '@shared/entities/object-attributes/graphql/queries/objectManagerFrontendAttributes.api'
 import frontendObjectAttributes from '@shared/entities/ticket/__tests__/mocks/frontendObjectAttributes.json'
 
@@ -405,6 +406,7 @@ describe('Form.vue - Edge Cases', () => {
           {
             type: 'group',
             name: 'adress',
+            isGroupOrList: true,
             children: [
               {
                 type: 'text',
@@ -527,6 +529,80 @@ describe('Form.vue - with object attributes', () => {
   })
 })
 
+describe('Form.vue - Multi step form groups', () => {
+  it('check for flat value structure for multi step form group submit', async () => {
+    const submitCallbackSpy = vi.fn()
+
+    const wrapper = await renderForm({
+      props: {
+        id: 'multi-step-form',
+        multiStepFormGroups: ['step1', 'step2'],
+        schema: [
+          {
+            type: 'group',
+            name: 'step1',
+            isGroupOrList: true,
+            children: [
+              {
+                type: 'text',
+                name: 'street',
+                label: 'Street',
+              },
+              {
+                type: 'text',
+                name: 'city',
+                label: 'City',
+              },
+            ],
+          },
+          {
+            type: 'text',
+            name: 'other',
+            label: 'Other',
+            value: 'Some text',
+          },
+          {
+            type: 'group',
+            name: 'step2',
+            isGroupOrList: true,
+            children: [
+              {
+                type: 'text',
+                name: 'title',
+                label: 'Title',
+              },
+              {
+                type: 'text',
+                name: 'fullname',
+                label: 'Fullname',
+                value: 'John Doe',
+              },
+            ],
+          },
+        ],
+        onSubmit: (data: FormValues) => submitCallbackSpy(data),
+      },
+    })
+
+    await wrapper.events.type(wrapper.getByLabelText('Street'), 'Street 12')
+
+    getNode('multi-step-form')?.submit()
+
+    await waitForNextTick(true)
+
+    expect(wrapper.emitted().submit).toBeTruthy()
+
+    expect(submitCallbackSpy).toHaveBeenCalledWith({
+      formId: expect.any(String),
+      street: 'Street 12',
+      city: undefined,
+      other: 'Some text',
+      title: undefined,
+      fullname: 'John Doe',
+    })
+  })
+})
+
 describe('Form.vue - Empty', () => {
   it('check for no form output without a schema with fields', () => {
     const wrapper = renderComponent(Form, {