Browse Source

Feature: Mobile - Added online notification functionality.

Co-authored-by: Dominik Klein <dk@zammad.com>
Mantas Masalskis 2 years ago
parent
commit
73b4b9e8bb

+ 4 - 0
app/frontend/apps/mobile/components/layout/LayoutBottomNavigation.vue

@@ -3,10 +3,12 @@
 import CommonUserAvatar from '@shared/components/CommonUserAvatar/CommonUserAvatar.vue'
 import { useSessionStore } from '@shared/stores/session'
 import { storeToRefs } from 'pinia'
+import { useOnlineNotificationCount } from '@shared/entities/online-notification/composables/useOnlineNotificationCount'
 import { useCustomLayout } from './useCustomLayout'
 
 const { user } = storeToRefs(useSessionStore())
 const { isCustomLayout } = useCustomLayout()
+const { unseenCount } = useOnlineNotificationCount()
 </script>
 
 <template>
@@ -23,10 +25,12 @@ const { isCustomLayout } = useCustomLayout()
       >
         <CommonIcon name="home" size="small" />
       </CommonLink>
+      <!-- TODO: instead of read icon, we need a number like in Figma -->
       <CommonLink
         link="/notifications"
         exact-active-class="text-blue"
         class="flex flex-1 justify-center"
+        :class="{ 'text-red': unseenCount > 0 }"
       >
         <CommonIcon name="bell" size="medium" />
       </CommonLink>

+ 5 - 0
app/frontend/apps/mobile/components/layout/__tests__/LayoutBottomNavigation.spec.ts

@@ -1,11 +1,16 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+import { OnlineNotificationsCountDocument } from '@shared/entities/online-notification/graphql/subscriptions/onlineNotificationsCount.api'
 import { useSessionStore } from '@shared/stores/session'
 import type { UserData } from '@shared/types/store'
 import { renderComponent } from '@tests/support/components'
+import { mockGraphQLSubscription } from '@tests/support/mock-graphql-api'
 import { flushPromises } from '@vue/test-utils'
 import LayoutBottomNavigation from '../LayoutBottomNavigation.vue'
 
+// TODO: Add correct notification count test case, when real count output exists.
+mockGraphQLSubscription(OnlineNotificationsCountDocument)
+
 describe('bottom navigation in layout', () => {
   it('renders navigation', async () => {
     const view = renderComponent(LayoutBottomNavigation, {

+ 3 - 4
app/frontend/apps/mobile/composables/useEditedBy.ts

@@ -1,5 +1,6 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+import { userDisplayName } from '@shared/entities/user/utils/getUserDisplayName'
 import { i18n } from '@shared/i18n'
 import { useSessionStore } from '@shared/stores/session'
 import type { Ref } from 'vue'
@@ -22,10 +23,8 @@ export const useEditedBy = (entity: Ref<Entity>) => {
   const author = computed(() => {
     const { updatedBy } = entity.value
     if (!updatedBy) return ''
-    return (
-      updatedBy.fullname ||
-      [updatedBy.firstname, updatedBy.lastname].filter(Boolean).join(' ')
-    )
+
+    return userDisplayName(updatedBy)
   })
 
   const date = computed(() => {

+ 3 - 6
app/frontend/apps/mobile/entities/ticket/stores/ticketOverviews.ts

@@ -25,12 +25,9 @@ export const useTicketOverviewsStore = defineStore('ticketOverviews', () => {
   const overviews = computed(() => {
     if (!overviewsRaw.value?.ticketOverviews.edges) return []
 
-    return (
-      overviewsRaw.value.ticketOverviews.edges
-        .filter((overview) => overview?.node?.id)
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        .map((edge) => edge!.node!)
-    )
+    return overviewsRaw.value.ticketOverviews.edges
+      .filter((overview) => overview?.node?.id)
+      .map((edge) => edge.node)
   })
 
   const overviewsByKey = computed(() => keyBy(overviews.value, 'id'))

+ 0 - 16
app/frontend/apps/mobile/pages/notifications/__tests__/mocks.ts

@@ -1,16 +0,0 @@
-// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
-
-import type { NotificationListItem } from '../types/notificaitons'
-
-export const getMockedNotification = (
-  item: Partial<NotificationListItem> = {},
-): NotificationListItem => {
-  return {
-    id: '154362',
-    title: 'State changed to closed',
-    user: { id: '2', lastname: 'Biden', firstname: 'Joe' },
-    read: false,
-    createdAt: new Date().toUTCString(),
-    ...item,
-  }
-}

+ 0 - 30
app/frontend/apps/mobile/pages/notifications/__tests__/selecting-notifications.spec.ts

@@ -1,30 +0,0 @@
-// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
-
-import { visitView } from '@tests/support/components/visitView'
-
-describe('visiting /notifications', () => {
-  test('can mark all notification as read', async () => {
-    const view = await visitView('/notifications')
-
-    console.log = vi.fn()
-
-    await view.events.click(view.getByText('Mark all as read'))
-
-    expect(console.log).toHaveBeenCalledWith('mark read', ['154362', '253223'])
-  })
-
-  test('deleting notifications actually deletes them', async () => {
-    const view = await visitView('/notifications')
-
-    console.log = vi.fn()
-    await view.events.click(view.getAllByIconName('trash')[0])
-
-    expect(console.log).toHaveBeenCalledWith(
-      'remove',
-      expect.objectContaining({ id: '154362' }),
-    )
-  })
-
-  // TODO no test, because api is mocked
-  test.todo("doesn't show 'remove all' if all are read")
-})

+ 0 - 60
app/frontend/apps/mobile/pages/notifications/components/NotificationItem.vue

@@ -1,60 +0,0 @@
-<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
-
-<script setup lang="ts">
-import CommonUserAvatar from '@shared/components/CommonUserAvatar/CommonUserAvatar.vue'
-import type { NotificationListItem } from '../types/notificaitons'
-
-defineProps<{
-  notification: NotificationListItem
-}>()
-
-defineEmits<{
-  (e: 'remove', notification: NotificationListItem): void
-}>()
-</script>
-
-<template>
-  <div class="flex">
-    <div class="flex items-center ltr:pr-2 rtl:pl-2">
-      <CommonIcon
-        name="trash"
-        class="cursor-pointer text-red"
-        size="tiny"
-        @click="$emit('remove', notification)"
-      />
-    </div>
-    <div class="flex items-center ltr:pr-2 rtl:pl-2">
-      <div
-        class="h-3 w-3 rounded-full"
-        :class="{ 'bg-blue': !notification.read }"
-        data-test-id="notificationRead"
-      ></div>
-    </div>
-    <div class="flex flex-1 border-b border-white/10 py-4">
-      <div class="flex items-center ltr:mr-4 rtl:ml-4">
-        <CommonUserAvatar :entity="notification.user" />
-      </div>
-
-      <div class="flex flex-col">
-        <div class="flex leading-4 text-gray-100">
-          #{{ notification.id }}
-          <div class="px-1">·</div>
-          <!-- TODO what name? -->
-          Name
-        </div>
-        <div class="text-lg leading-5">
-          <strong
-            >{{ notification.title
-            }}{{ notification.message ? ': ' : '' }}</strong
-          >{{ notification.message ? `“${notification.message}”` : '' }}
-        </div>
-        <div class="mt-1 flex text-gray">
-          <!-- TODO what name? -->
-          Gina
-          <div class="px-1">·</div>
-          <CommonDateTime :date-time="notification.createdAt" type="relative" />
-        </div>
-      </div>
-    </div>
-  </div>
-</template>

+ 0 - 77
app/frontend/apps/mobile/pages/notifications/components/__tests__/NotificationItem.spec.ts

@@ -1,77 +0,0 @@
-// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
-
-const date = new Date('2020-01-01 00:00:00')
-
-vi.setSystemTime(date)
-
-import { renderComponent } from '@tests/support/components'
-import { getMockedNotification } from '../../__tests__/mocks'
-import NotificationItem from '../NotificationItem.vue'
-
-describe('notification item', () => {
-  test('renders correctly', async () => {
-    const notification = getMockedNotification({
-      read: true,
-      title: 'Title',
-      createdAt: new Date('2019-12-30 00:00:00').toISOString(),
-    })
-
-    const view = renderComponent(NotificationItem, {
-      props: {
-        notification,
-      },
-      form: true,
-    })
-
-    expect(view.getByTestId('notificationRead')).not.toHaveClass('bg-blue')
-
-    await view.rerender({
-      notification: {
-        ...notification,
-        read: false,
-      },
-    })
-
-    expect(view.getByTestId('notificationRead')).toHaveClass('bg-blue')
-
-    expect(view.getByText('JB'), 'has avatar').toBeInTheDocument()
-    expect(
-      view.getByText(new RegExp(`#${notification.id}`)),
-      'has id',
-    ).toBeInTheDocument()
-
-    expect(view.getByText(/^Title$/), 'has title').toBeInTheDocument()
-    expect(view.getByText(/2 days ago/)).toBeInTheDocument()
-
-    await view.rerender({
-      notification: {
-        ...notification,
-        message: 'Some Message',
-      },
-    })
-
-    expect(view.getByText(/Title:/)).toBeInTheDocument()
-    expect(view.getByText(/“Some Message”/)).toBeInTheDocument()
-
-    vi.useRealTimers()
-
-    await view.events.click(view.getByIconName('trash'))
-
-    expect(view.emitted().remove).toBeDefined()
-  })
-
-  test('can delete notification', async () => {
-    const notification = getMockedNotification()
-
-    const view = renderComponent(NotificationItem, {
-      props: {
-        notification,
-      },
-      form: true,
-    })
-
-    await view.events.click(view.getByIconName('trash'))
-
-    expect(view.emitted().remove).toBeDefined()
-  })
-})

+ 0 - 12
app/frontend/apps/mobile/pages/notifications/types/notificaitons.ts

@@ -1,12 +0,0 @@
-// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
-
-import type { AvatarUser } from '@shared/components/CommonUserAvatar'
-
-export interface NotificationListItem {
-  id: string
-  read: boolean
-  user: AvatarUser
-  title: string
-  message?: string
-  createdAt: string
-}

+ 0 - 68
app/frontend/apps/mobile/pages/notifications/views/NotificationsList.vue

@@ -1,68 +0,0 @@
-<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
-
-<script setup lang="ts">
-// TODO remove eslint-disable
-/* eslint-disable zammad/zammad-detect-translatable-string */
-import { computed, ref } from 'vue'
-import LayoutCustomNavigation from '@mobile/components/layout/LayoutCustomNavigation.vue'
-import { useHeader } from '@mobile/composables/useHeader'
-import NotificationItem from '../components/NotificationItem.vue'
-import type { NotificationListItem } from '../types/notificaitons'
-
-useHeader({
-  backUrl: '/',
-})
-
-// TODO make actual API call
-// TODO subscribe to notification changes
-const notifications = ref<NotificationListItem[]>([
-  {
-    id: '154362',
-    title: 'State changed to closed',
-    user: { id: '2', lastname: 'Biden', firstname: 'Joe' },
-    read: false,
-    createdAt: new Date().toUTCString(),
-  },
-  {
-    id: '253223',
-    title: 'Created internal note',
-    message: 'Please give me a minute to check with our developers.',
-    user: { id: '3', lastname: 'Rock', firstname: 'John' },
-    read: true,
-    createdAt: new Date(2022, 1, 1).toUTCString(),
-  },
-])
-
-const markAllRead = () => {
-  console.log(
-    'mark read',
-    notifications.value.map(({ id }) => id),
-  )
-}
-
-const removeNotification = (notification: NotificationListItem) => {
-  console.log('remove', notification)
-}
-
-const haveUnread = computed(() => notifications.value.some((n) => !n.read))
-</script>
-
-<template>
-  <div class="ltr:pr-4 ltr:pl-3 rtl:pl-4 rtl:pr-3">
-    <NotificationItem
-      v-for="notification of notifications"
-      :key="notification.id"
-      :notification="notification"
-      @remove="removeNotification($event)"
-    />
-
-    <LayoutCustomNavigation v-if="haveUnread">
-      <div
-        class="flex flex-1 cursor-pointer justify-center text-base text-blue"
-        @click="markAllRead"
-      >
-        {{ $t('Mark all as read') }}
-      </div>
-    </LayoutCustomNavigation>
-  </div>
-</template>

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