Browse Source

Fix: Mobile - Format dates, improve types of object attributes

Vladimir Sheremet 2 years ago
parent
commit
0217a15123

+ 7 - 11
app/frontend/apps/mobile/components/CommonObjectAttributes/AttributeDate/AttributeDate.vue

@@ -1,23 +1,19 @@
 <!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
-import { i18n } from '@shared/i18n'
-import { computed } from 'vue'
+import CommonDateTime from '@shared/components/CommonDateTime/CommonDateTime.vue'
 import type { ObjectAttributeDate } from './attributeDateTypes'
 
-const props = defineProps<{
+defineProps<{
   attribute: ObjectAttributeDate
   value: string
 }>()
-
-const body = computed(() => {
-  if (props.attribute.dataType === 'date') {
-    return i18n.date(props.value)
-  }
-  return i18n.dateTime(props.value)
-})
 </script>
 
 <template>
-  {{ body }}
+  <CommonDateTime
+    :date-time="value"
+    :type="attribute.dataType === 'date' ? 'absolute' : 'configured'"
+    :absolute-format="attribute.dataType"
+  />
 </template>

+ 4 - 1
app/frontend/apps/mobile/components/CommonObjectAttributes/AttributeDate/attributeDateTypes.ts

@@ -7,7 +7,10 @@ export interface ObjectAttributeDate extends ObjectManagerFrontendAttribute {
   dataOption: {
     relation: string
     null: boolean
+    past?: boolean
+    future?: boolean
+    include_timezone?: boolean
     default: Maybe<string>
-    diff: Maybe<string>
+    diff: Maybe<number>
   }
 }

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

@@ -1,6 +1,7 @@
 <!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
+import { phoneify } from '@shared/utils/formatter'
 import { computed } from 'vue'
 import type { ObjectAttributeInput } from './attributeInputTypes'
 
@@ -14,7 +15,8 @@ const link = computed(() => {
   // link is processed in common component
   if (linktemplate) return null
   const value = String(props.value)
-  if (type === 'tel') return `tel:${value.replace(/[^0-9+]/g, '')}`
+  // app/assets/javascripts/app/index.coffee:135
+  if (type === 'tel') return `tel:${phoneify(value)}`
   if (type === 'url') return value
   if (type === 'email') return `mailto:${value}`
   return ''

+ 4 - 2
app/frontend/apps/mobile/components/CommonObjectAttributes/AttributeInput/attributeInputTypes.ts

@@ -7,8 +7,10 @@ export interface ObjectAttributeInput extends ObjectManagerFrontendAttribute {
   dataOption: {
     item_class: string
     maxlength: number
-    linktemplate?: string
+    autocapitalize?: boolean
     null: boolean
-    type: 'text' | 'url' | 'email' | 'tel' // text
+    type: 'text' | 'url' | 'email' | 'tel'
+    linktemplate?: string
+    note?: string
   }
 }

+ 2 - 0
app/frontend/apps/mobile/components/CommonObjectAttributes/AttributeRichtext/attributeRichtextTypes.ts

@@ -10,6 +10,8 @@ export interface ObjectAttributeRichtext
     no_images: boolean
     note: string
     null: boolean
+    upload?: boolean
+    rows?: number
     type: string // text
   }
 }

+ 5 - 0
app/frontend/apps/mobile/components/CommonObjectAttributes/AttributeSingleSelect/attributeSingleSelectTypes.ts

@@ -12,6 +12,11 @@ export interface ObjectAttributeSingleSelect
     null: boolean
     nulloption: boolean
     relation: string
+    permission?: string[]
+    multiple: boolean
+    filter?: number[] // ids
+    only_shown_if_selectable?: boolean
+    relation_condition?: { access?: 'full'; roles?: 'Agent' }
     // array for tree_select
     // irrelevant for displaying
     options: Record<string, string> | Record<string, string>[]

+ 19 - 18
app/frontend/apps/mobile/components/CommonObjectAttributes/CommonObjectAttributes.vue

@@ -27,7 +27,7 @@ const attributesDeclarations = import.meta.glob<AttributeDeclaration>(
   { eager: true, import: 'default' },
 )
 
-const componentsByType = Object.values(attributesDeclarations).reduce(
+const definitionsByType = Object.values(attributesDeclarations).reduce(
   (acc, declaration) => {
     declaration.dataTypes.forEach((type) => {
       acc[type] = declaration.component
@@ -70,7 +70,22 @@ interface AttributeField {
 
 const fields = computed<AttributeField[]>(() => {
   return props.attributes
-    .filter((attribute) => {
+    .map((attribute) => {
+      let value = getValue(attribute.name)
+
+      if (typeof value !== 'boolean' && !value) {
+        value = attribute.dataOption?.default
+      }
+
+      return {
+        attribute,
+        component: definitionsByType[attribute.dataType],
+        value,
+      }
+    })
+    .filter(({ attribute, value, component }) => {
+      if (!component) return false
+
       const dataOption = attribute.dataOption || {}
 
       if (
@@ -80,30 +95,16 @@ const fields = computed<AttributeField[]>(() => {
         return false
       }
 
-      // hide all falsy non-boolean values without default value
+      // hide all falsy non-boolean values without value
       if (
         !['boolean', 'active'].includes(attribute.dataType) &&
-        isEmpty(dataOption.default) &&
-        isEmpty(getValue(attribute.name))
+        isEmpty(value)
       ) {
         return false
       }
 
       return !skipAttributes.includes(attribute.name)
     })
-    .map((attribute) => {
-      const component = componentsByType[attribute.dataType]
-      const value = getValue(attribute.name)
-
-      return {
-        attribute,
-        component,
-        value:
-          typeof value === 'boolean'
-            ? value
-            : value || attribute.dataOption?.default,
-      }
-    })
 })
 </script>
 

+ 43 - 1
app/frontend/apps/mobile/components/CommonObjectAttributes/__tests__/CommonObjectAttributes.spec.ts

@@ -1,10 +1,14 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+vi.setSystemTime('2021-04-09T10:11:12Z')
+
 import type { ObjectManagerFrontendAttribute } from '@shared/graphql/types'
 import { i18n } from '@shared/i18n'
 import { getByRole } from '@testing-library/vue'
 import { renderComponent } from '@tests/support/components'
+import { mockApplicationConfig } from '@tests/support/mock-applicationConfig'
 import { mockPermissions } from '@tests/support/mock-permissions'
+import { flushPromises } from '@vue/test-utils'
 import { keyBy } from 'lodash-es'
 import CommonObjectAttributes from '../CommonObjectAttributes.vue'
 
@@ -402,7 +406,7 @@ describe('common object attributes interface', () => {
     expect(getRegion('Note')).toHaveTextContent(object.note)
     expect(getRegion('Active')).toHaveTextContent('yes')
 
-    expect(getRegion('Date Attribute')).toHaveTextContent('19/08/2022')
+    expect(getRegion('Date Attribute')).toHaveTextContent(/19\/08\/2022$/)
     expect(getRegion('Textarea Field')).toHaveTextContent('textarea text')
     expect(getRegion('Integer Field')).toHaveTextContent('600')
     expect(getRegion('DateTime Field')).toHaveTextContent('11/08/2022 05:00')
@@ -600,4 +604,42 @@ describe('common object attributes interface', () => {
     expect(singleTreeSelect).toHaveTextContent('llave1::llave1_niño1')
     expect(multiTreeSelect).toHaveTextContent('llave1, llave1::llave1_niño1')
   })
+
+  it('renders different dates', async () => {
+    const object = {
+      now: '2021-04-09T10:11:12Z',
+      past: '2021-02-09T10:11:12Z',
+      future: '2021-10-09T10:11:12Z',
+    }
+
+    const attributes = [
+      { ...attributesByKey.date_time_field, name: 'now', display: 'now' },
+      { ...attributesByKey.date_time_field, name: 'past', display: 'past' },
+      { ...attributesByKey.date_time_field, name: 'future', display: 'future' },
+    ]
+
+    const view = renderComponent(CommonObjectAttributes, {
+      props: {
+        object,
+        attributes,
+      },
+      router: true,
+    })
+
+    const getRegion = (time: string) => view.getByRole('region', { name: time })
+
+    expect(getRegion('now')).toHaveTextContent('2021-04-09 10:11')
+    expect(getRegion('past')).toHaveTextContent('2021-02-09 10:11')
+    expect(getRegion('future')).toHaveTextContent('2021-10-09 10:11')
+
+    mockApplicationConfig({
+      pretty_date_format: 'relative',
+    })
+
+    await flushPromises()
+
+    expect(getRegion('now')).toHaveTextContent('just now')
+    expect(getRegion('past')).toHaveTextContent('1 month ago')
+    expect(getRegion('future')).toHaveTextContent('in 6 months')
+  })
 })

+ 6 - 4
app/frontend/shared/components/CommonDateTime/CommonDateTime.vue

@@ -37,13 +37,15 @@ const outputAbsoluteDate = computed(() => {
 </script>
 
 <template>
-  <span v-if="outputFormat === 'absolute'" data-test-id="date-time-absolute">
+  <time v-if="outputFormat === 'absolute'" data-test-id="date-time-absolute">
     {{ outputAbsoluteDate }}
-  </span>
-  <span
+  </time>
+  <time
     v-else
     :title="i18n.dateTime(dateTime)"
+    :datetime="dateTime"
     data-test-id="date-time-relative"
-    >{{ i18n.relativeDateTime(dateTime) }}</span
   >
+    {{ i18n.relativeDateTime(dateTime) }}
+  </time>
 </template>

+ 8 - 0
app/frontend/shared/i18n/dates.ts

@@ -143,3 +143,11 @@ export const relativeDateTime = (
 
   return translator.translate('just now')
 }
+
+export const getDateFormat = (translator: Translator) => {
+  return translator.lookup('FORMAT_DATE') || 'yyyy-mm-dd'
+}
+
+export const getDateTimeFormat = (translator: Translator) => {
+  return translator.lookup('FORMAT_DATETIME') || 'yyyy-mm-dd HH:MM'
+}

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