Browse Source

Feature: Mobile - Improve online notification handling without relation object permission.

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

+ 44 - 0
app/frontend/apps/mobile/pages/online-notification/__tests__/online-notification-actions.spec.ts

@@ -89,6 +89,50 @@ describe('selecting a online notification', () => {
     expect(view.container).not.toHaveTextContent('Mark all as read')
   })
 
+  it('can mark notification without relation behind (no longer permission) as read', async () => {
+    const mockApi = mockGraphQLApi(OnlineNotificationsDocument).willResolve(
+      mockOnlineNotificationQuery([
+        {
+          seen: false,
+        },
+        {
+          seen: false,
+          metaObject: null,
+          createdBy: null,
+        },
+        {
+          seen: false,
+        },
+      ]),
+    )
+
+    const view = await visitView('/notifications')
+
+    await triggerNextOnlineNotificationCount(2)
+
+    await waitUntil(() => mockApi.calls.resolve)
+
+    mockGraphQLApi(OnlineNotificationMarkAllAsSeenDocument).willResolve({
+      onlineNotificationMarkAllAsSeen: {
+        errors: null,
+        onlineNotifications: [
+          {
+            id: '2',
+            seen: true,
+            __typename: 'OnlineNotification',
+          },
+        ],
+      },
+    })
+
+    const noRelationNotificationItem = view.getByText(
+      'You can no longer see the ticket.',
+    )
+    await view.events.click(noRelationNotificationItem)
+
+    expect(view.getAllByLabelText('Notification read')).toHaveLength(1)
+  })
+
   it('can delete online notification', async () => {
     const mockApi = mockGraphQLApi(OnlineNotificationsDocument).willResolve([
       mockOnlineNotificationQuery([

+ 43 - 0
app/frontend/apps/mobile/pages/online-notification/__tests__/online-notification-list.spec.ts

@@ -111,4 +111,47 @@ describe('selecting a online notification', () => {
       '/tickets/111',
     )
   })
+
+  it('shows a list of online notifications which includes also items without permission to the relation', async () => {
+    const mockApi = mockGraphQLApi(OnlineNotificationsDocument).willResolve(
+      mockOnlineNotificationQuery([
+        {
+          metaObject: {
+            __typename: 'Ticket',
+            id: '111',
+            internalId: 111,
+            title: 'Ticket Title 1',
+          },
+        },
+        {
+          metaObject: null,
+          createdBy: null,
+        },
+        {
+          seen: true,
+          metaObject: {
+            __typename: 'Ticket',
+            id: '333',
+            internalId: 333,
+            title: 'Ticket Title 3',
+          },
+        },
+      ]),
+    )
+
+    const view = await visitView('/notifications')
+
+    await waitUntil(() => mockApi.calls.resolve)
+
+    const notificationItems = view.getAllByText('Ticket Title', {
+      exact: false,
+    })
+
+    expect(notificationItems).toHaveLength(2)
+
+    const noRelationNotificationItems = view.getAllByText(
+      'You can no longer see the ticket.',
+    )
+    expect(noRelationNotificationItems).toHaveLength(1)
+  })
 })

+ 4 - 2
app/frontend/apps/mobile/pages/online-notification/components/NotificationItem.vue

@@ -10,15 +10,16 @@ export interface Props {
   objectName: string
   typeName: string
   seen: boolean
-  metaObject: ActivityMessageMetaObject
+  metaObject?: Maybe<ActivityMessageMetaObject>
   createdAt: string
-  createdBy: AvatarUser
+  createdBy?: Maybe<AvatarUser>
 }
 
 defineProps<Props>()
 
 defineEmits<{
   (e: 'remove', id: Scalars['ID']): void
+  (e: 'seen', id: Scalars['ID']): void
 }>()
 </script>
 
@@ -46,6 +47,7 @@ defineEmits<{
       :created-at="createdAt"
       :created-by="createdBy"
       :meta-object="metaObject"
+      @seen="$emit('seen', itemId)"
     />
   </div>
 </template>

+ 16 - 0
app/frontend/apps/mobile/pages/online-notification/components/__tests__/NotificationItem.spec.ts

@@ -66,4 +66,20 @@ describe('NotificationItem.vue', () => {
     const emittedRemove = view.emitted().remove as Array<Array<Scalars['ID']>>
     expect(emittedRemove[0][0]).toBe('111')
   })
+
+  it('should emit "seen" event on click for none linked notifications', async () => {
+    const view = renderNotificationItem({
+      metaObject: undefined,
+      createdBy: undefined,
+    })
+
+    const item = view.getByText('You can no longer see the ticket.')
+
+    await view.events.click(item)
+
+    expect(view.emitted().seen).toBeTruthy()
+
+    const emittedSeen = view.emitted().seen as Array<Array<Scalars['ID']>>
+    expect(emittedSeen[0][0]).toBe('111')
+  })
 })

+ 18 - 0
app/frontend/apps/mobile/pages/online-notification/views/NotificationsList.vue

@@ -60,6 +60,23 @@ const removeNotification = (id: Scalars['ID']) => {
   removeNotificationMutation.send()
 }
 
+const seenNotification = (id: Scalars['ID']) => {
+  const seenNotificationMutation = new MutationHandler(
+    useOnlineNotificationMarkAllAsSeenMutation({
+      variables: { onlineNotificationIds: [id] },
+    }),
+    {
+      errorNotificationMessage: __(
+        'The online notifcation could not be marked as seen.',
+      ),
+    },
+  )
+
+  mutationTriggered = true
+
+  seenNotificationMutation.send()
+}
+
 const markingAsSeen = ref(false)
 
 const markAllRead = async () => {
@@ -112,6 +129,7 @@ const haveUnread = computed(() => unseenCount.value > 0)
         :created-by="notification.createdBy"
         :meta-object="notification.metaObject"
         @remove="removeNotification"
+        @seen="seenNotification"
       />
 
       <div v-if="!notifications.length" class="px-4 py-3 text-center text-base">

+ 23 - 7
app/frontend/shared/components/ActivityMessage/ActivityMessage.vue

@@ -9,15 +9,16 @@ import type { ActivityMessageMetaObject } from '@shared/graphql/types'
 import { userDisplayName } from '@shared/entities/user/utils/getUserDisplayName'
 import { markup } from '@shared/utils/markup'
 import CommonUserAvatar from '../CommonUserAvatar/CommonUserAvatar.vue'
+import CommonAvatar from '../CommonAvatar/CommonAvatar.vue'
 import type { AvatarUser } from '../CommonUserAvatar'
 import { activityMessageBuilder } from './builders'
 
 export interface Props {
   typeName: string
   objectName: string
-  metaObject: ActivityMessageMetaObject
+  metaObject?: Maybe<ActivityMessageMetaObject>
   createdAt: string
-  createdBy: AvatarUser
+  createdBy?: Maybe<AvatarUser>
 }
 
 const props = defineProps<Props>()
@@ -29,25 +30,40 @@ if (!builder.value) {
 
 const message = builder.value?.messageText(
   props.typeName,
-  userDisplayName(props.createdBy),
+  props.createdBy ? userDisplayName(props.createdBy) : '',
   props.metaObject,
 )
 
+const link = props.metaObject
+  ? builder.value?.path(props.metaObject)
+  : undefined
+
 if (builder.value && !message) {
   log.error(
     `Unknow action for (${props.objectName}/${props.typeName}), extend activityMessages() of model.`,
   )
 }
+
+defineEmits<{
+  (e: 'seen'): void
+}>()
 </script>
 
 <template>
-  <CommonLink
+  <component
+    :is="link ? 'CommonLink' : 'div'"
     v-if="builder"
     class="flex flex-1 border-b border-white/10 py-4"
-    :link="builder.path(metaObject)"
+    :link="link"
+    @click="!link ? $emit('seen') : undefined"
   >
     <div class="flex items-center ltr:mr-4 rtl:ml-4">
-      <CommonUserAvatar :entity="createdBy" />
+      <CommonUserAvatar v-if="createdBy" :entity="createdBy" />
+      <CommonAvatar
+        v-else
+        class="bg-red-bright text-white"
+        icon="mobile-lock"
+      />
     </div>
 
     <div class="flex flex-col">
@@ -56,5 +72,5 @@ if (builder.value && !message) {
         <CommonDateTime :date-time="createdAt" type="relative" />
       </div>
     </div>
-  </CommonLink>
+  </component>
 </template>

+ 25 - 0
app/frontend/shared/components/ActivityMessage/__tests__/ActivityMessage.spec.ts

@@ -75,6 +75,31 @@ describe('NotificationItem.vue', () => {
     expect(view.getByText(/2 days ago/)).toBeInTheDocument()
   })
 
+  it('check that default message and avatar for no meta object is visible', () => {
+    const view = renderActivityMessage({
+      metaObject: undefined,
+      createdBy: undefined,
+    })
+
+    expect(view.container).toHaveTextContent(
+      'You can no longer see the ticket.',
+    )
+    expect(view.getByIconName('mobile-lock')).toBeInTheDocument()
+  })
+
+  it('should emit "seen" event on click for none linked notifications', async () => {
+    const view = renderActivityMessage({
+      metaObject: undefined,
+      createdBy: undefined,
+    })
+
+    const item = view.getByText('You can no longer see the ticket.')
+
+    await view.events.click(item)
+
+    expect(view.emitted().seen).toBeTruthy()
+  })
+
   it('no output for not existing builder', (context) => {
     context.skipConsole = true
 

+ 1 - 0
app/frontend/shared/components/ActivityMessage/builders/__tests__/activityMessageBuilder.spec.ts

@@ -10,6 +10,7 @@ describe('activity message builder are available', () => {
 
     expect(models).toContain('Ticket')
     expect(models).toContain('User')
+    expect(models).toContain('Organization')
     expect(models).toContain('Group')
   })
 })

+ 5 - 1
app/frontend/shared/components/ActivityMessage/builders/data-privacy-task.ts

@@ -12,8 +12,12 @@ const path = (metaObject: DataPrivacyTask) => {
 const messageText = (
   type: string,
   authorName: string,
-  metaObject: DataPrivacyTask,
+  metaObject?: DataPrivacyTask,
 ): Maybe<string> => {
+  if (!metaObject) {
+    return i18n.t('You can no longer see the data privacy task.')
+  }
+
   const objectTitle = metaObject.deletableId || '-'
 
   switch (type) {

+ 5 - 1
app/frontend/shared/components/ActivityMessage/builders/group.ts

@@ -12,8 +12,12 @@ const path = (metaObject: Group) => {
 const messageText = (
   messageType: string,
   authorName: string,
-  metaObject: Group,
+  metaObject?: Group,
 ): Maybe<string> => {
+  if (!metaObject) {
+    return i18n.t('You can no longer see the group.')
+  }
+
   const objectTitle = metaObject.name || '-'
 
   switch (messageType) {

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