Browse Source

Feature: Mobile - Continue working on the form handling.

Dominik Klein 3 years ago
parent
commit
3e0e88b333

+ 1 - 1
.eslintrc.js

@@ -124,7 +124,7 @@ module.exports = {
     'import/resolver': {
       alias: {
         map: [
-          ['@', path.resolve(__dirname, './app/frontend/')],
+          ['@', path.resolve(__dirname, './app/frontend')],
           ['@mobile', path.resolve(__dirname, './app/frontend/apps/mobile')],
           ['@common', path.resolve(__dirname, './app/frontend/common')],
           ['@tests', path.resolve(__dirname, './app/frontend/tests')],

+ 13 - 0
.storybook/preview.ts

@@ -7,6 +7,9 @@ import { i18n } from '@common/utils/i18n'
 import { app } from '@storybook/vue3'
 import 'virtual:svg-icons-register' // eslint-disable-line import/no-unresolved
 import initializeStore from '@common/stores'
+import initializeForm, { getFormPlugins } from '@common/form'
+import type { ImportGlobEagerOutput } from '@common/types/utils'
+import type { FormKitPlugin } from '@formkit/core'
 import { createRouter, createWebHashHistory, type Router } from 'vue-router'
 
 // Adds the translations to storybook.
@@ -16,6 +19,16 @@ app.config.globalProperties.i18n = i18n
 initializeGlobalComponents(app)
 initializeStore(app)
 
+// Initialize the FormKit plugin witht he needed fields ands internal FormKit plugins.
+const mobilePluginModules: ImportGlobEagerOutput<FormKitPlugin> =
+  import.meta.globEager('../app/frontend/apps/mobile/form/plugins/*.ts')
+const mobileFieldModules: ImportGlobEagerOutput<FormFieldTypeImportModules> =
+  import.meta.globEager(
+    '../app/frontend/apps/mobile/components/form/field/**/*.ts',
+  )
+const plugins = getFormPlugins(mobilePluginModules)
+initializeForm(app, mobileFieldModules, plugins)
+
 const router: Router = createRouter({
   history: createWebHashHistory(),
   routes: [],

+ 2 - 2
app/frontend/apps/mobile/form/index.ts

@@ -5,8 +5,8 @@ import type {
   FormFieldTypeImportModules,
   InitializeAppForm,
 } from '@common/types/form'
-import { ImportGlobEagerOutput } from '@common/types/utils'
-import { FormKitPlugin } from '@formkit/core'
+import type { ImportGlobEagerOutput } from '@common/types/utils'
+import type { FormKitPlugin } from '@formkit/core'
 import { App } from 'vue'
 
 const pluginModules: ImportGlobEagerOutput<FormKitPlugin> =

+ 68 - 22
app/frontend/apps/mobile/views/Login.vue

@@ -2,44 +2,39 @@
 
 <template>
   <!-- TODO: Only a dummy implementation for the login... -->
-  <div class="flex h-full min-h-screen flex-col items-center justify-center">
-    <div class="w-full max-w-md">
+  <div class="flex h-full min-h-screen flex-col items-center px-7 pt-7 pb-4">
+    <div class="m-auto w-full max-w-md">
       <div class="flex grow flex-col justify-center">
-        <div class="my-5 grow p-8">
+        <div class="my-5 grow">
           <div class="flex justify-center p-2">
             <CommonLogo />
           </div>
           <div class="mb-6 flex justify-center p-2 text-2xl font-extrabold">
-            {{ 'Zammad' }}
+            {{ applicationConfig.value.product_name }}
           </div>
-          <template v-if="authenticationConfig.value.maintenance_login">
+          <template v-if="applicationConfig.value.maintenance_login">
             <!-- eslint-disable vue/no-v-html -->
             <div
               class="my-1 flex items-center rounded bg-green py-2 px-4 text-white"
-              v-html="authenticationConfig.value.maintenance_login_message"
+              v-html="applicationConfig.value.maintenance_login_message"
             ></div>
           </template>
           <Form
-            id="testing"
             v-bind:schema="formSchema"
             class="text-left"
             v-on:submit="login"
           >
             <template v-slot:after-fields>
-              <div class="mt-1 flex grow items-baseline justify-between">
-                <a class="cursor-pointer select-none text-yellow underline">
-                  {{ i18n.t('Register') }}
-                </a>
-
-                <a class="cursor-pointer select-none text-yellow">
-                  {{ i18n.t('Forgot password?') }}
-                </a>
+              <div class="mt-4 flex grow items-center justify-center">
+                <span class="ltr:mr-1 rtl:ml-1">{{ i18n.t('New user?') }}</span>
+                <CommonLink
+                  v-bind:link="'TODO'"
+                  class="cursor-pointer select-none !text-yellow underline"
+                  >{{ i18n.t('Register') }}</CommonLink
+                >
               </div>
-            </template>
-            <template v-slot:buttons>
               <FormKit
-                v-bind:ignore="true"
-                wrapper-class="flex grow justify-center items-center mx-8 mt-8"
+                wrapper-class="mx-8 mt-8 flex grow justify-center items-center"
                 input-class="py-2 px-4 w-full h-14 text-xl font-semibold text-black bg-yellow rounded select-none"
                 type="submit"
               >
@@ -50,6 +45,30 @@
         </div>
       </div>
     </div>
+    <div class="mb-6 flex items-center justify-center">
+      <CommonLink link="TODO" class="!text-gray underline">
+        {{ i18n.t('Continue to desktop app') }}
+      </CommonLink>
+    </div>
+    <div class="flex items-center justify-center align-middle text-gray-200">
+      <CommonLink
+        link="https://zammad.org"
+        is-external
+        open-in-new-tab
+        class="ltr:mr-1 rtl:ml-1"
+      >
+        <CommonIcon name="logo" v-bind:fixed-size="{ width: 24, height: 24 }" />
+      </CommonLink>
+      <span class="ltr:mr-1 rtl:ml-1">{{ i18n.t('Powered by') }}</span>
+      <CommonLink
+        link="https://zammad.org"
+        is-external
+        open-in-new-tab
+        class="font-semibold !text-gray-200"
+      >
+        Zammad
+      </CommonLink>
+    </div>
   </div>
 </template>
 
@@ -105,7 +124,7 @@ const formSchema = [
     placeholder: __('Username / Email'),
     wrapperClass: 'relative floating-input',
     inputClass:
-      'block mt-1 w-full h-14 text-sm bg-gray-300 rounded border-none focus:outline-none placeholder:text-transparent',
+      'block mt-1 w-full h-14 text-sm bg-gray-500 rounded border-none focus:outline-none placeholder:text-transparent',
     labelClass:
       'absolute top-0 left-0 py-5 px-3 h-full text-base transition-all duration-100 ease-in-out origin-left pointer-events-none',
     sectionsSchema: forFloatingLabel,
@@ -118,12 +137,39 @@ const formSchema = [
     placeholder: __('Password'),
     wrapperClass: 'relative floating-input',
     inputClass:
-      'block mt-1 w-full h-14 text-sm bg-gray-300 rounded border-none focus:outline-none placeholder:text-transparent',
+      'block mt-1 w-full h-14 text-sm bg-gray-500 rounded border-none focus:outline-none placeholder:text-transparent',
     labelClass:
       'absolute top-0 left-0 py-5 px-3 h-full text-base transition-all duration-100 ease-in-out origin-left pointer-events-none',
     sectionsSchema: forFloatingLabel,
     validation: 'required',
   },
+  {
+    isLayout: true,
+    element: 'div',
+    attrs: {
+      class: 'mt-2 flex grow items-center justify-between text-white',
+    },
+    children: [
+      {
+        type: 'checkbox',
+        label: __('Remember me'),
+        name: 'remember_me',
+        wrapperClass: 'inline-flex items-center',
+        inputClass:
+          'appearance-none h-4 w-4 border-[1.5px] border-white rounded-sm bg-transparent',
+        innerClass: 'mr-2',
+      },
+      {
+        isLayout: true,
+        component: 'CommonLink',
+        props: {
+          class: 'text-right !text-white',
+          link: 'TODO',
+        },
+        children: i18n.t('Forgot password?'),
+      },
+    ],
+  },
 ]
 
 interface FormData {
@@ -147,7 +193,7 @@ const login = (formData: FormKitGroupValue): void => {
     })
 }
 
-const authenticationConfig = useApplicationConfigStore()
+const applicationConfig = useApplicationConfigStore()
 </script>
 
 <style lang="postcss">

+ 4 - 1
app/frontend/common/components/common/CommonHelloWorld.vue

@@ -12,7 +12,10 @@
 import { i18n } from '@common/utils/i18n'
 import { computed } from 'vue'
 
-defineProps<{ msg: string; show: boolean }>()
+defineProps<{
+  msg: string
+  show: boolean
+}>()
 
 const manualTranslation = computed(() => {
   return i18n.t('The second component.')

+ 1 - 1
app/frontend/common/components/common/CommonLink.vue

@@ -75,7 +75,7 @@ const linkClass = computed(() => {
   let classes = 'text-blue hover:underline'
 
   if (props.disabled) {
-    classes += ' pointer-events-none text-gray-100/75'
+    classes += ' pointer-events-none text-gray-300/75'
   }
 
   return classes

+ 70 - 23
app/frontend/common/components/form/Form.vue

@@ -2,26 +2,31 @@
 
 <template>
   <FormKit
-    v-if="Object.keys(schemaData.fields).length > 0"
+    v-if="Object.keys(schemaData.fields).length > 0 || $slots.fields"
     v-bind:id="formId"
     v-model="values"
     type="form"
     v-bind:config="formConfig"
     v-bind:form-class="localClass"
     v-bind:actions="false"
+    v-bind:incomplete-message="false"
     v-bind:plugins="localFormKitPlugins"
     v-bind:sections-schema="formKitSectionsSchema"
     v-on:node="setFormNode"
     v-on:submit="onSubmit"
   >
     <slot name="before-fields" />
-    <FormKitSchema
-      v-bind:schema="staticSchema"
-      v-bind:data="schemaData"
-      v-bind:library="additionalComponentLibrary"
-    />
+    <template v-if="!$slots.fields">
+      <FormKitSchema
+        v-bind:schema="staticSchema"
+        v-bind:data="schemaData"
+        v-bind:library="additionalComponentLibrary"
+      />
+    </template>
+    <template v-else>
+      <slot name="fields" />
+    </template>
     <slot name="after-fields" />
-    <slot name="buttons" />
   </FormKit>
 </template>
 
@@ -53,6 +58,8 @@ import type {
   FormKitSchemaCondition,
   FormKitNode,
   FormKitClasses,
+  FormKitSchemaDOMNode,
+  FormKitSchemaComponent,
 } from '@formkit/core'
 import getUuid from '@common/utils/getUuid'
 
@@ -164,7 +171,9 @@ const updateSchemaDataField = (field: FormSchemaField) => {
 }
 
 const buildStaticSchema = (schema: FormSchemaNode[]) => {
-  const buildFormKitField = (field: FormSchemaField) => {
+  const buildFormKitField = (
+    field: FormSchemaField,
+  ): FormKitSchemaComponent => {
     return {
       $cmp: 'FormKit',
       if: `$fields.${field.name}.show`,
@@ -178,26 +187,64 @@ const buildStaticSchema = (schema: FormSchemaNode[]) => {
     }
   }
 
-  schema.forEach((field) => {
-    if ((field as FormSchemaLayout).isLayout) {
-      const layoutItem = field as FormSchemaLayout
-      const childrens = layoutItem.children.map((childField) => {
-        updateSchemaDataField(childField)
+  const getLayoutType = (
+    layoutItem: FormSchemaLayout,
+  ): FormKitSchemaDOMNode | FormKitSchemaComponent => {
+    if ('component' in layoutItem) {
+      return {
+        $cmp: layoutItem.component,
+        props: layoutItem.props,
+      }
+    }
 
-        return buildFormKitField(childField)
-      })
+    return {
+      $el: layoutItem.element,
+      attrs: layoutItem.attrs,
+    }
+  }
 
-      staticSchema.push({
-        $cmp: 'FormLayout',
-        props: layoutItem.props,
-        children: childrens,
-      })
+  schema.forEach((node) => {
+    if ((node as FormSchemaLayout).isLayout) {
+      const layoutItem = node as FormSchemaLayout
+
+      if (typeof layoutItem.children === 'string') {
+        staticSchema.push({
+          ...getLayoutType(layoutItem),
+          children: layoutItem.children,
+        })
+      } else {
+        const childrens = layoutItem.children.map((childNode) => {
+          if (typeof childNode === 'string') {
+            return childNode
+          }
+          if ((childNode as FormSchemaLayout).isLayout) {
+            return {
+              ...getLayoutType(childNode as FormSchemaLayout),
+              children: childNode.children as
+                | string
+                | FormKitSchemaNode[]
+                | FormKitSchemaCondition,
+            }
+          }
+
+          updateSchemaDataField(childNode as FormSchemaField)
+          return buildFormKitField(childNode as FormSchemaField)
+        })
+
+        staticSchema.push({
+          ...getLayoutType(layoutItem),
+          children: childrens,
+        })
+      }
     } else {
-      const localField = field as FormSchemaField
+      const field = node as FormSchemaField
+
+      // TODO: maybe we can also add better support for Group and List fields, when this bug is fixed:
+      // https://github.com/formkit/formkit/issues/91
 
-      staticSchema.push(buildFormKitField(localField))
+      staticSchema.push(buildFormKitField(field))
 
-      updateSchemaDataField(localField)
+      updateSchemaDataField(field)
     }
   })
 }

+ 11 - 0
app/frontend/common/components/form/field/FieldCheckbox.ts

@@ -0,0 +1,11 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import initializeFieldDefinition from '@common/form/core/initializeFieldDefinition'
+import { checkbox as checkboxDefinition } from '@formkit/inputs'
+
+initializeFieldDefinition(checkboxDefinition)
+
+export default {
+  fieldType: 'checkbox',
+  definition: checkboxDefinition,
+}

+ 53 - 2
app/frontend/common/components/form/field/FieldPassword.ts

@@ -1,11 +1,62 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
 import initializeFieldDefinition from '@common/form/core/initializeFieldDefinition'
+import { FormKitExtendableSchemaRoot, FormKitNode } from '@formkit/core'
 import { password as passwordDefinition } from '@formkit/inputs'
+import { cloneDeep } from 'lodash-es'
 
-initializeFieldDefinition(passwordDefinition)
+const localPasswordDefinition = cloneDeep(passwordDefinition)
+
+const switchPasswordVisibility = (node: FormKitNode) => {
+  const { props } = node
+
+  if (!props.definition) return
+
+  const definition = cloneDeep(props.definition)
+
+  props.passwordVisibilityIcon = 'eye'
+
+  const originalSchema = definition.schema as FormKitExtendableSchemaRoot
+
+  definition.schema = (extensions) => {
+    const localExtensions = {
+      ...extensions,
+      suffix: {
+        $el: 'span',
+        children: [
+          {
+            $cmp: 'CommonIcon',
+            props: {
+              // TODO: we need to add the new icon from figma.
+              name: '$passwordVisibilityIcon',
+              key: node.name,
+              class: 'absolute top-1/2 transform -translate-y-1/2 right-3',
+              size: 'small',
+              onClick: () => {
+                props.type = props.type === 'password' ? 'text' : 'password'
+              },
+            },
+          },
+        ],
+      },
+    }
+    return originalSchema(localExtensions)
+  }
+
+  props.definition = definition
+
+  node.on('prop:type', ({ payload, origin }) => {
+    const { props } = origin
+    props.passwordVisibilityIcon = payload === 'password' ? 'eye' : 'cog' // TODO: align icon name to eye-off?
+  })
+}
+
+initializeFieldDefinition(localPasswordDefinition, {
+  props: ['passwordVisibilityIcon'],
+  features: [switchPasswordVisibility],
+})
 
 export default {
   fieldType: 'password',
-  definition: passwordDefinition,
+  definition: localPasswordDefinition,
 }

+ 2 - 0
app/frontend/common/components/form/field/FieldSelect.ts

@@ -3,6 +3,8 @@
 import initializeFieldDefinition from '@common/form/core/initializeFieldDefinition'
 import { select as selectDefinition } from '@formkit/inputs'
 
+// TODO: at the moment only the FormKit-BuildIn, but will be replaces with a own version.
+
 initializeFieldDefinition(selectDefinition)
 
 export default {

Some files were not shown because too many files changed in this diff