Просмотр исходного кода

Feature: Mobile - Add dummy search page

Vladimir Sheremet 2 лет назад
Родитель
Сommit
97e73871cc

+ 1 - 0
.storybook/config/preview.ts

@@ -19,6 +19,7 @@ import type {
 
 // Adds the translations to storybook.
 app.config.globalProperties.i18n = i18n
+app.config.globalProperties.$t = i18n.t.bind(i18n)
 
 // Initialize the needed core components and plugins.
 initializeGlobalComponents(app)

+ 49 - 0
app/frontend/apps/mobile/components/Organization/OrganizationItem.stories.ts

@@ -0,0 +1,49 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import createTemplate from '@stories/support/createTemplate'
+import OrganizationItem, { type Props } from './OrganizationItem.vue'
+
+export default {
+  title: 'Organization/OrganizationItem',
+  component: OrganizationItem,
+}
+
+const Template = createTemplate<Props>(OrganizationItem)
+
+const organization = {
+  id: '54321',
+  ticketsCount: 2,
+  name: 'Lorem Ipsum',
+  active: false,
+}
+
+export const Default = Template.create({
+  entity: {
+    ...organization,
+    active: true,
+    updatedAt: new Date(2022, 1, 2).toISOString(),
+    updatedBy: {
+      id: '456',
+      firstname: 'Jane',
+      lastname: 'Doe',
+    },
+    members: [
+      {
+        lastname: 'Wise',
+        firstname: 'Erik',
+      },
+      {
+        lastname: 'Smith',
+        firstname: 'Peter',
+      },
+      {
+        lastname: "O'Hara",
+        firstname: 'Nils',
+      },
+    ],
+  },
+})
+
+export const NoEdit = Template.create({
+  entity: organization,
+})

+ 65 - 0
app/frontend/apps/mobile/components/Organization/OrganizationItem.vue

@@ -0,0 +1,65 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import { useEditedBy } from '@mobile/composables/useEditedBy'
+import CommonOrganizationAvatar from '@shared/components/CommonOrganizationAvatar/CommonOrganizationAvatar.vue'
+import { computed, toRef } from 'vue'
+import { OrganizationItemData } from './types'
+
+export interface Props {
+  entity: OrganizationItemData
+}
+
+const props = defineProps<Props>()
+
+const { stringUpdated } = useEditedBy(toRef(props, 'entity'))
+
+const users = computed(() => {
+  const { members } = props.entity
+  if (!members) return ''
+
+  let usersString = members
+    .slice(0, 2)
+    .map((user) => {
+      return [user.firstname, user.lastname].filter(Boolean).join(' ')
+    })
+    .join(', ')
+
+  const length = members.length - 2
+
+  if (length > 0) {
+    usersString += `, +${length}`
+  }
+
+  return usersString
+})
+</script>
+
+<template>
+  <div class="flex">
+    <div class="mt-4 w-12">
+      <CommonOrganizationAvatar class="bg-gray" :entity="entity" />
+    </div>
+    <div
+      class="flex flex-1 flex-col border-b border-white/10 py-3 text-gray-100"
+    >
+      <div class="flex">
+        <div>
+          {{
+            entity.ticketsCount === 1
+              ? `1 ${$t('ticket')}`
+              : $t('%s tickets', entity.ticketsCount)
+          }}
+        </div>
+        <div v-if="users" class="px-1">·</div>
+        <div>{{ users }}</div>
+      </div>
+      <div class="mb-1 text-lg font-bold">
+        <slot> {{ entity.name }} </slot>
+      </div>
+      <div v-if="stringUpdated" data-test-id="stringUpdated" class="text-gray">
+        {{ stringUpdated }}
+      </div>
+    </div>
+  </div>
+</template>

+ 84 - 0
app/frontend/apps/mobile/components/Organization/__tests__/OrganizationItem.spec.ts

@@ -0,0 +1,84 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+const now = new Date(2022, 1, 1, 20, 0, 0, 0)
+vi.setSystemTime(now)
+
+import { renderComponent } from '@tests/support/components'
+import { OrganizationItemData } from '../types'
+import OrganizationItem from '../OrganizationItem.vue'
+
+describe('ticket item display', () => {
+  afterEach(() => {
+    vi.useRealTimers()
+  })
+
+  it('renders correctly', () => {
+    const now = new Date(2022, 1, 1)
+    vi.setSystemTime(now)
+
+    const organization: OrganizationItemData = {
+      id: '54321',
+      ticketsCount: 2,
+      name: 'lorem ipsum',
+      active: true,
+      members: [
+        {
+          lastname: 'Wise',
+          firstname: 'Erik',
+        },
+        {
+          lastname: 'Smith',
+          firstname: 'Peter',
+        },
+        {
+          lastname: "O'Hara",
+          firstname: 'Nils',
+        },
+      ],
+      updatedAt: new Date(2022, 1, 1, 10, 0, 0, 0).toISOString(),
+      updatedBy: {
+        id: '456',
+        firstname: 'Jane',
+        lastname: 'Doe',
+      },
+    }
+
+    const view = renderComponent(OrganizationItem, {
+      props: {
+        entity: organization,
+      },
+      store: true,
+    })
+
+    expect(view.getByText('lorem ipsum')).toBeInTheDocument()
+    expect(view.getByText('2 tickets')).toBeInTheDocument()
+    expect(view.getByText('·')).toBeInTheDocument()
+    expect(view.getByText('Erik Wise, Peter Smith, +1')).toBeInTheDocument()
+
+    expect(
+      view.getByText('edited 10 hours ago by Jane Doe'),
+    ).toBeInTheDocument()
+  })
+
+  it('renders when something is missing', () => {
+    const organization: OrganizationItemData = {
+      id: '54321',
+      ticketsCount: 1,
+      name: 'lorem ipsum',
+      active: true,
+    }
+
+    const view = renderComponent(OrganizationItem, {
+      props: {
+        entity: organization,
+      },
+      store: true,
+    })
+
+    expect(view.getByText('lorem ipsum')).toBeInTheDocument()
+    expect(view.getByText('1 ticket')).toBeInTheDocument()
+    expect(view.queryIconByName('·')).not.toBeInTheDocument()
+
+    expect(view.queryByTestId('stringUpdated')).not.toBeInTheDocument()
+  })
+})

+ 18 - 0
app/frontend/apps/mobile/components/Organization/types.ts

@@ -0,0 +1,18 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+export interface OrganizationItemData {
+  id: string
+  ticketsCount: number
+  members?: {
+    lastname?: Maybe<string>
+    firstname?: Maybe<string>
+  }[]
+  active: boolean
+  name: string
+  updatedAt?: string
+  updatedBy?: {
+    id: string
+    firstname?: Maybe<string>
+    lastname?: Maybe<string>
+  }
+}

+ 42 - 0
app/frontend/apps/mobile/components/Ticket/TicketItem.stories.ts

@@ -0,0 +1,42 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { TicketState } from '@shared/entities/ticket/types'
+import createTemplate from '@stories/support/createTemplate'
+import TicketItem, { type Props } from './TicketItem.vue'
+
+export default {
+  title: 'Ticket/TicketItem',
+  component: TicketItem,
+}
+
+const Template = createTemplate<Props>(TicketItem)
+
+const ticket = {
+  id: '54321',
+  number: '12345',
+  state: TicketState.Open,
+  title: 'Test Ticket',
+  owner: {
+    firstname: 'John',
+    lastname: 'Doe',
+  },
+}
+
+export const Default = Template.create({
+  entity: {
+    ...ticket,
+    updatedAt: new Date(2022, 1, 2).toISOString(),
+    updatedBy: {
+      id: '456',
+      firstname: 'Jane',
+      lastname: 'Doe',
+    },
+    priority: {
+      name: 'HIGH',
+    },
+  },
+})
+
+export const NoEdit = Template.create({
+  entity: ticket,
+})

+ 96 - 0
app/frontend/apps/mobile/components/Ticket/TicketItem.vue

@@ -0,0 +1,96 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import { computed, toRef } from 'vue'
+import CommonTicketStateIndicator from '@shared/components/CommonTicketStateIndicator/CommonTicketStateIndicator.vue'
+import { useEditedBy } from '@mobile/composables/useEditedBy'
+import { type TicketItemData } from './types'
+
+export interface Props {
+  entity: TicketItemData
+}
+
+interface Priority {
+  class: string | null
+  text: string
+}
+
+const props = defineProps<Props>()
+
+const { stringUpdated } = useEditedBy(toRef(props, 'entity'))
+
+// TODO
+const priority = computed<Priority | null>(() => {
+  const { entity } = props
+  if (!entity.priority) {
+    return null
+  }
+  return {
+    class: entity.priority.uiColor
+      ? `u-${entity.priority.uiColor}-color`
+      : `u-default-color`,
+    text: entity.priority.name.toUpperCase(),
+  }
+})
+</script>
+
+<template>
+  <div class="flex">
+    <div class="flex w-12 items-center justify-center">
+      <!-- TODO label? -->
+      <CommonTicketStateIndicator
+        :status="entity.state"
+        :label="entity.state"
+      />
+    </div>
+    <div
+      class="flex flex-1 items-center border-b border-white/10 py-3 text-gray-100"
+    >
+      <div class="flex-1">
+        <div class="flex">
+          <div>#{{ entity.id }}</div>
+          <div v-if="entity.owner" class="px-1">·</div>
+          <div v-if="entity.owner">
+            {{ entity.owner.firstname }} {{ entity.owner.lastname }}
+          </div>
+        </div>
+        <div class="mb-1 text-lg font-bold">
+          <slot>
+            {{ entity.title }}
+          </slot>
+        </div>
+        <div
+          v-if="stringUpdated"
+          data-test-id="stringUpdated"
+          class="text-gray"
+        >
+          {{ stringUpdated }}
+        </div>
+      </div>
+      <div
+        v-if="priority"
+        :class="[priority.class, 'h-min rounded-[4px] py-1 px-2']"
+      >
+        {{ priority.text }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.u-default-color {
+  @apply bg-gray/10 text-gray;
+}
+
+.u-high-priority-color {
+  @apply bg-red/10 text-red;
+}
+
+.u-low-priority-color {
+  @apply bg-blue/10 text-blue;
+}
+
+.u-medium-priority-color {
+  @apply bg-yellow/10 text-yellow;
+}
+</style>

+ 84 - 0
app/frontend/apps/mobile/components/Ticket/__tests__/TicketItem.spec.ts

@@ -0,0 +1,84 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+const now = new Date(2022, 1, 1, 20, 0, 0, 0)
+vi.setSystemTime(now)
+
+import { renderComponent } from '@tests/support/components'
+import { TicketState } from '@shared/entities/ticket/types'
+import { TicketItemData } from '../types'
+import TicketItem from '../TicketItem.vue'
+
+describe('ticket item display', () => {
+  afterEach(() => {
+    vi.useRealTimers()
+  })
+
+  it('renders correctly', () => {
+    const ticket: TicketItemData = {
+      id: '54321',
+      number: '12345',
+      state: TicketState.Open,
+      title: 'test ticket',
+      owner: {
+        firstname: 'John',
+        lastname: 'Doe',
+      },
+      updatedAt: new Date(2022, 1, 1, 10, 0, 0, 0).toISOString(),
+      updatedBy: {
+        id: '456',
+        firstname: 'Jane',
+        lastname: 'Doe',
+      },
+      priority: {
+        name: 'high',
+        uiColor: 'high-priority',
+      },
+    }
+
+    const view = renderComponent(TicketItem, {
+      props: {
+        entity: ticket,
+      },
+      store: true,
+    })
+
+    expect(view.getByAltText(TicketState.Open)).toBeInTheDocument()
+    expect(view.getByText('#54321')).toBeInTheDocument()
+    expect(view.getByText('·')).toBeInTheDocument()
+    expect(view.getByText('John Doe')).toBeInTheDocument()
+    expect(view.getByText('test ticket')).toBeInTheDocument()
+
+    expect(
+      view.getByText('edited 10 hours ago by Jane Doe'),
+    ).toBeInTheDocument()
+
+    const priority = view.getByText('HIGH')
+
+    expect(priority).toBeInTheDocument()
+    expect(priority).toHaveClass('u-high-priority-color')
+  })
+
+  it('renders when something is missing', () => {
+    const ticket: TicketItemData = {
+      id: '54321',
+      number: '12345',
+      state: TicketState.Open,
+      title: 'test ticket',
+    }
+
+    const view = renderComponent(TicketItem, {
+      props: {
+        entity: ticket,
+      },
+      store: true,
+    })
+
+    expect(view.getByText('#54321')).toBeInTheDocument()
+    expect(view.queryByText('·')).not.toBeInTheDocument()
+    expect(view.getByText('test ticket')).toBeInTheDocument()
+
+    expect(view.queryByTestId('stringUpdated')).not.toBeInTheDocument()
+
+    expect(view.queryByText('HIGH')).not.toBeInTheDocument()
+  })
+})

+ 26 - 0
app/frontend/apps/mobile/components/Ticket/types.ts

@@ -0,0 +1,26 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { TicketState } from '@shared/entities/ticket/types'
+
+// TODO 2022-05-31 Sheremet V.A. base types on actual usage
+export interface TicketItemData {
+  id: string
+  title: string
+  number: string
+  state: TicketState
+  // TODO 2022-06-01 Sheremet V.A.
+  priority?: {
+    name: string
+    uiColor?: Maybe<string>
+  }
+  owner?: {
+    firstname?: Maybe<string>
+    lastname?: Maybe<string>
+  }
+  updatedAt?: string
+  updatedBy?: {
+    id: string
+    firstname?: Maybe<string>
+    lastname?: Maybe<string>
+  }
+}

+ 37 - 0
app/frontend/apps/mobile/components/User/UserItem.stories.ts

@@ -0,0 +1,37 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import createTemplate from '@stories/support/createTemplate'
+import UserItem, { type Props } from './UserItem.vue'
+
+export default {
+  title: 'User/UserItem',
+  component: UserItem,
+}
+
+const Template = createTemplate<Props>(UserItem)
+
+const user = {
+  id: '123',
+  ticketsCount: 2,
+  firstname: 'John',
+  lastname: 'Doe',
+  organization: {
+    name: 'organization',
+  },
+}
+
+export const Default = Template.create({
+  entity: {
+    ...user,
+    updatedAt: new Date(2022, 1, 2).toISOString(),
+    updatedBy: {
+      id: '456',
+      firstname: 'Jane',
+      lastname: 'Doe',
+    },
+  },
+})
+
+export const NoEdit = Template.create({
+  entity: user,
+})

Некоторые файлы не были показаны из-за большого количества измененных файлов