Browse Source

Feature: Mobile - Allow editing organization

Vladimir Sheremet 2 years ago
parent
commit
b70e60e0f6

+ 6 - 1
app/frontend/apps/mobile/components/CommonObjectAttributes/AttributeBoolean/AttributeBoolean.vue

@@ -13,8 +13,13 @@ const body = computed(() => {
   const { true: yes, false: no } = props.attribute.dataOption?.options || {}
   return props.value ? yes || __('yes') : no || __('no')
 })
+
+const translate = computed(() => {
+  const { translate = true } = props.attribute.dataOption || {}
+  return translate
+})
 </script>
 
 <template>
-  {{ attribute.dataOption.translate ? $t(body) : body }}
+  {{ translate ? $t(body) : body }}
 </template>

+ 3 - 6
app/frontend/apps/mobile/components/CommonObjectAttributes/CommonObjectAttributes.vue

@@ -55,7 +55,8 @@ const isEmpty = (value: unknown) => {
   if (Array.isArray(value)) {
     return value.length === 0
   }
-  return !value
+  // null or undefined or ''
+  return value == null || value === ''
 }
 
 const session = useSessionStore()
@@ -95,11 +96,7 @@ const fields = computed<AttributeField[]>(() => {
         return false
       }
 
-      // hide all falsy non-boolean values without value
-      if (
-        !['boolean', 'active'].includes(attribute.dataType) &&
-        isEmpty(value)
-      ) {
+      if (isEmpty(value)) {
         return false
       }
 

+ 3 - 0
app/frontend/apps/mobile/components/CommonObjectAttributes/__tests__/CommonObjectAttributes.spec.ts

@@ -126,6 +126,9 @@ describe('common object attributes interface', () => {
     expect(
       view.queryByRole('region', { name: 'Invisible' }),
     ).not.toBeInTheDocument()
+    expect(
+      view.queryByRole('region', { name: 'Hidden Boolean' }),
+    ).not.toBeInTheDocument()
   })
 
   test('hides attributes without permission', () => {

+ 15 - 0
app/frontend/apps/mobile/components/CommonObjectAttributes/__tests__/attributes.json

@@ -42,6 +42,21 @@
     },
     "__typename": "ObjectManagerFrontendAttribute"
   },
+  {
+    "name": "hiddenBoolean",
+    "display": "Hidden Boolean",
+    "dataType": "boolean",
+    "dataOption": {
+      "null": true,
+      "default": null,
+      "item_class": "formGroup--halfSize",
+      "options": {
+        "false": "no",
+        "true": "yes"
+      }
+    },
+    "__typename": "ObjectManagerFrontendAttribute"
+  },
   {
     "name": "note",
     "display": "Note",

+ 43 - 87
app/frontend/apps/mobile/components/Organization/OrganizationEditDialog.vue

@@ -4,15 +4,12 @@
 import { defineFormSchema } from '@mobile/form/composable'
 import Form from '@shared/components/Form/Form.vue'
 import CommonDialog from '@mobile/components/CommonDialog/CommonDialog.vue'
-import { CheckboxVariant } from '@shared/components/Form/fields/FieldCheckbox'
 import type { ConfidentTake } from '@shared/types/utils'
-import {
-  // EnumObjectManagerObjects,
-  type OrganizationInput,
-} from '@shared/graphql/types'
+import { EnumObjectManagerObjects } from '@shared/graphql/types'
 import type { OrganizationQuery } from '@shared/graphql/types'
 import { closeDialog } from '@shared/composables/useDialog'
 import { MutationHandler } from '@shared/server/apollo/handler'
+import { type FormData } from '@shared/components/Form/types'
 import { shallowRef } from 'vue'
 import type { FormKitNode } from '@formkit/core'
 import { useOrganizationUpdateMutation } from '@mobile/entities/organization/graphql/mutations/update.api'
@@ -24,85 +21,16 @@ interface Props {
 
 const props = defineProps<Props>()
 
-// TODO get from backend
 const schema = defineFormSchema([
   {
-    isLayout: true,
-    component: 'FormGroup',
-    children: [
-      {
-        type: 'checkbox',
-        name: 'shared',
-        props: {
-          variant: CheckboxVariant.Switch,
-        },
-        label: __('Shared organization'),
-        value: props.organization.shared,
-      },
-      {
-        type: 'checkbox',
-        name: 'domainAssignment',
-        props: {
-          variant: CheckboxVariant.Switch,
-        },
-        label: __('Domain based assignment'),
-        value: props.organization.domainAssignment,
-      },
-      // TODO disabled based on domainAssignment
-      {
-        type: 'text',
-        name: 'domain',
-        label: __('Domain'),
-      },
-    ],
+    name: 'name',
+    required: true,
+    object: EnumObjectManagerObjects.Organization,
   },
   {
-    isLayout: true,
-    component: 'FormGroup',
-    children: [
-      {
-        type: 'textarea',
-        name: 'note',
-        label: __('Note'),
-        value: props.organization.note,
-      },
-    ],
+    screen: 'edit',
+    object: EnumObjectManagerObjects.Organization,
   },
-  {
-    isLayout: true,
-    component: 'FormGroup',
-    children: [
-      {
-        type: 'checkbox',
-        name: 'active',
-        props: {
-          variant: CheckboxVariant.Switch,
-        },
-        label: __('Active'),
-        value: props.organization.active,
-      },
-    ],
-  },
-  // {
-  //   isLayout: true,
-  //   component: 'FormGroup',
-  //   children: [
-  //     {
-  //       screen: 'edit',
-  //       object: EnumObjectManagerObjects.Organization,
-  //     },
-  //   ],
-  // },
-  // {
-  //   isLayout: true,
-  //   component: 'FormGroup',
-  //   children: [
-  //     {
-  //       name: 'active',
-  //       object: EnumObjectManagerObjects.Organization,
-  //     },
-  //   ],
-  // },
 ])
 
 const updateQuery = new MutationHandler(useOrganizationUpdateMutation({}))
@@ -110,15 +38,43 @@ const formElement = shallowRef<{ formNode: FormKitNode }>()
 
 const submitForm = () => formElement.value?.formNode.submit()
 
-const saveOrganization = async (input: OrganizationInput) => {
+interface OrganizationForm {
+  domain: string
+  domain_assignment: boolean
+  note: string
+  name: string
+  shared: boolean
+  active: boolean
+}
+
+const initialValue = {
+  ...props.organization,
+  ...props.organization.objectAttributeValues?.reduce(
+    (acc, { attribute, value }) => ({ ...acc, [attribute.name]: value }),
+    {},
+  ),
+}
+
+const saveOrganization = async (formData: FormData<OrganizationForm>) => {
+  const objectAttributeValues = props.organization.objectAttributeValues?.map(
+    ({ attribute }) => {
+      return {
+        name: attribute.name,
+        value: formData[attribute.name],
+      }
+    },
+  )
+
   const result = await updateQuery.send({
     id: props.organization.id,
     input: {
-      domain: input.domain,
-      domainAssignment: input.domainAssignment,
-      note: input.note,
-      shared: input.shared,
-      active: input.active,
+      name: formData.name,
+      domain: formData.domain,
+      domainAssignment: formData.domain_assignment,
+      note: formData.note,
+      shared: formData.shared,
+      active: formData.active,
+      objectAttributeValues,
     },
   })
   // close only if there are no errors
@@ -144,10 +100,10 @@ const saveOrganization = async (input: OrganizationInput) => {
       id="edit-organization"
       ref="formElement"
       class="w-full p-4"
-      :initial-values="props.organization"
       :schema="schema"
+      :initial-values="initialValue"
       use-object-attributes
-      @submit="saveOrganization($event as OrganizationInput)"
+      @submit="saveOrganization($event as FormData<OrganizationForm>)"
     />
   </CommonDialog>
 </template>

+ 34 - 5
app/frontend/apps/mobile/components/Organization/__tests__/OrganizationEditDialog.spec.ts

@@ -5,10 +5,26 @@ import { renderComponent } from '@tests/support/components'
 import { mockGraphQLApi } from '@tests/support/mock-graphql-api'
 import { waitUntil } from '@tests/support/utils'
 import { OrganizationUpdateDocument } from '@mobile/entities/organization/graphql/mutations/update.api'
+import { mockOrganizationObjectAttributes } from '@mobile/entities/organization/__tests__/mocks/organization-mocks'
 import OrganizationEditDialog from '../OrganizationEditDialog.vue'
 
 vi.mock('@shared/composables/useDialog')
 
+const textareaAttribute = {
+  name: 'textarea',
+  display: 'Textarea Field',
+  dataType: 'textarea',
+  dataOption: {
+    default: '',
+    maxlength: 500,
+    rows: 4,
+    null: true,
+    options: {},
+    relation: '',
+  },
+  __typename: 'ObjectManagerFrontendAttribute',
+}
+
 const createUpdateMock = () =>
   mockGraphQLApi(OrganizationUpdateDocument).willResolve({
     organizationUpdate: {
@@ -20,6 +36,9 @@ const createUpdateMock = () =>
         domainAssignment: true,
         active: true,
         note: 'Save something as this note',
+        objectAttributeValues: [
+          { attribute: textareaAttribute, value: 'new value' },
+        ],
       },
       errors: null,
     },
@@ -37,6 +56,9 @@ const renderEditDialog = () =>
         domain: '',
         note: '',
         active: false,
+        objectAttributeValues: [
+          { attribute: textareaAttribute, value: 'old value' },
+        ],
       },
     },
     form: true,
@@ -46,22 +68,27 @@ const renderEditDialog = () =>
 
 describe('editing organization', () => {
   test('can edit organization', async () => {
+    const attributesApi = mockOrganizationObjectAttributes()
+
     const mockApi = createUpdateMock()
 
     const view = renderEditDialog()
 
+    await waitUntil(() => attributesApi.calls.resolve)
+
+    await view.events.type(view.getByLabelText('Name'), ' 2')
     await view.events.click(view.getByLabelText('Shared organization'))
     await view.events.click(view.getByLabelText('Domain based assignment'))
     await view.events.type(
       view.getByLabelText('Domain'),
       'some-domain@domain.me',
     )
-    await view.events.type(
-      view.getByLabelText('Note'),
-      'Save something as this note',
-    )
     await view.events.click(view.getByLabelText('Active'))
 
+    const textarea = view.getByLabelText('Textarea Field')
+    await view.events.clear(textarea)
+    await view.events.type(textarea, 'new value')
+
     await view.events.click(view.getByRole('button', { name: 'Save' }))
 
     await waitUntil(() => mockApi.calls.resolve)
@@ -69,11 +96,13 @@ describe('editing organization', () => {
     expect(mockApi.spies.resolve).toHaveBeenCalledWith({
       id: 'faked-id',
       input: {
+        name: 'Some Organization 2',
         shared: false,
         domain: 'some-domain@domain.me',
         domainAssignment: true,
         active: true,
-        note: 'Save something as this note',
+        note: '',
+        objectAttributeValues: [{ name: 'textarea', value: 'new value' }],
       },
     })
     expect(closeDialog).toHaveBeenCalled()

+ 1 - 1
app/frontend/apps/mobile/entities/organization/__tests__/mocks/organization-mocks.ts

@@ -157,7 +157,7 @@ export const organizationObjectAttributes = () => ({
     {
       __typename: 'ObjectManagerFrontendAttribute',
       name: 'textarea',
-      display: 'textarea',
+      display: 'Textarea Field',
       dataType: 'textarea',
       dataOption: {
         default: '',

+ 1 - 3
app/frontend/apps/mobile/form/theme/global/getCoreClasses.ts

@@ -41,9 +41,7 @@ const getCoreClasses: FormThemeExtension = (classes: FormThemeClasses) => {
       input: `${classes.textarea?.input} min-h-[100px]`,
     }),
     checkbox: {
-      wrapper: `${
-        classes.checkbox?.wrapper || ''
-      } w-full justify-between h-14 ltr:pl-2 rtl:pr-2`,
+      wrapper: `${classes.checkbox?.wrapper || ''} w-full justify-between`,
     },
     tags: addBlockFloatingLabel(classes.tags),
     select: addBlockFloatingLabel(classes.select),

+ 5 - 31
app/frontend/apps/mobile/pages/ticket/routes.ts

@@ -2,6 +2,8 @@
 
 import type { RouteRecordRaw } from 'vue-router'
 
+import { ticketInformationRoutes } from './views/TicketInformation/plugins'
+
 const routes: RouteRecordRaw[] = [
   {
     path: '/tickets/:internalId(\\d+)',
@@ -17,39 +19,11 @@ const routes: RouteRecordRaw[] = [
   },
   {
     path: '/tickets/:internalId(\\d+)/information',
-    component: () => import('./views/TicketInformationView.vue'),
+    component: () =>
+      import('./views/TicketInformation/TicketInformationView.vue'),
     name: 'TicketInformationView',
     props: true,
-    children: [
-      {
-        path: '',
-        name: 'TicketInformationDetails',
-        component: () => import('./views/TicketInformationDetails.vue'),
-        meta: {
-          requiresAuth: true,
-          requiredPermission: [],
-        },
-      },
-      {
-        path: 'customer',
-        name: 'TicketInformationCustomer',
-        props: (route) => ({ internalId: Number(route.params.internalId) }),
-        component: () => import('./views/TicketInformationCustomer.vue'),
-        meta: {
-          requiresAuth: true,
-          requiredPermission: [],
-        },
-      },
-      {
-        path: 'organization',
-        name: 'TicketInformationOrganization',
-        component: () => import('./views/TicketInformationOrganization.vue'),
-        meta: {
-          requiresAuth: true,
-          requiredPermission: [],
-        },
-      },
-    ],
+    children: ticketInformationRoutes,
     meta: {
       title: __('Ticket information'),
       requiresAuth: true,

+ 1 - 1
app/frontend/apps/mobile/pages/ticket/views/TicketInformationCustomer.vue → app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationCustomer.vue

@@ -13,7 +13,7 @@ import CommonLoader from '@mobile/components/CommonLoader/CommonLoader.vue'
 import CommonUserAvatar from '@shared/components/CommonUserAvatar/CommonUserAvatar.vue'
 import CommonOrganizationsList from '@mobile/components/CommonOrganizationsList/CommonOrganizationsList.vue'
 import { normalizeEdges } from '@shared/utils/helpers'
-import type { TicketById } from '../types/tickets'
+import type { TicketById } from '../../types/tickets'
 
 interface Props {
   internalId: number

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