From 3beb10c1c88849c25177f3213d543cd1df8119dc Mon Sep 17 00:00:00 2001
From: Coung Ngo <cngo@gitlab.com>
Date: Tue, 5 Dec 2023 08:12:52 +0000
Subject: [PATCH] Fix showing discussions in group work items

Behind feature flag namespace_level_work_items
---
 .../notes/work_item_note_actions.vue          |  2 +
 .../notes/work_item_note_awards_list.vue      |  2 +
 .../work_items/components/work_item_notes.vue |  6 ++-
 ...group_work_item_notes_by_iid.query.graphql | 32 ++++++++++++
 .../work_items/notes/award_utils.js           |  5 +-
 .../notes/work_item_note_actions_spec.js      |  1 +
 .../notes/work_item_note_awards_list_spec.js  | 51 +++++++++++--------
 .../components/work_item_notes_spec.js        | 27 ++++++++++
 .../work_items/notes/award_utils_spec.js      | 18 +++++++
 9 files changed, 121 insertions(+), 23 deletions(-)
 create mode 100644 app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql

diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index cb9a560f9e1bf..445938ad56a1f 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -33,6 +33,7 @@ export default {
   directives: {
     GlTooltip: GlTooltipDirective,
   },
+  inject: ['isGroup'],
   props: {
     fullPath: {
       type: String,
@@ -152,6 +153,7 @@ export default {
             note: this.note,
             name,
             fullPath: this.fullPath,
+            isGroup: this.isGroup,
             workItemIid: this.workItemIid,
           }),
         });
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index 75a8a7b29c0d0..c3b3c0e6db7f7 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -8,6 +8,7 @@ export default {
   components: {
     AwardsList,
   },
+  inject: ['isGroup'],
   props: {
     fullPath: {
       type: String,
@@ -73,6 +74,7 @@ export default {
             note: this.note,
             name,
             fullPath: this.fullPath,
+            isGroup: this.isGroup,
             workItemIid: this.workItemIid,
           }),
         });
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 6756acd44956a..83958ee4ef36d 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -28,6 +28,7 @@ import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_ite
 import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql';
 import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql';
 import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation.graphql';
+import groupWorkItemNotesByIidQuery from '../graphql/notes/group_work_item_notes_by_iid.query.graphql';
 import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql';
 import WorkItemAddNote from './notes/work_item_add_note.vue';
 
@@ -46,6 +47,7 @@ export default {
     WorkItemNotesActivityHeader,
     WorkItemHistoryOnlyFilterNote,
   },
+  inject: ['isGroup'],
   props: {
     fullPath: {
       type: String,
@@ -169,7 +171,9 @@ export default {
   },
   apollo: {
     workItemNotes: {
-      query: workItemNotesByIidQuery,
+      query() {
+        return this.isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery;
+      },
       variables() {
         return {
           fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql
new file mode 100644
index 0000000000000..f86176b28365c
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql
@@ -0,0 +1,32 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "ee_else_ce/work_items/graphql/notes/work_item_note.fragment.graphql"
+
+query groupWorkItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+  workspace: group(fullPath: $fullPath) {
+    id
+    workItems(iid: $iid) {
+      nodes {
+        id
+        iid
+        widgets {
+          ... on WorkItemWidgetNotes {
+            type
+            discussions(first: $pageSize, after: $after, filter: ALL_NOTES) {
+              pageInfo {
+                ...PageInfo
+              }
+              nodes {
+                id
+                notes {
+                  nodes {
+                    ...WorkItemNote
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/assets/javascripts/work_items/notes/award_utils.js b/app/assets/javascripts/work_items/notes/award_utils.js
index 5351a22d5934c..4f35b06a68568 100644
--- a/app/assets/javascripts/work_items/notes/award_utils.js
+++ b/app/assets/javascripts/work_items/notes/award_utils.js
@@ -5,6 +5,7 @@ import {
   updateCacheAfterAddingAwardEmojiToNote,
   updateCacheAfterRemovingAwardEmojiFromNote,
 } from '~/work_items/graphql/cache_utils';
+import groupWorkItemNotesByIidQuery from '../graphql/notes/group_work_item_notes_by_iid.query.graphql';
 import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql';
 import addAwardEmojiMutation from '../graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
 import removeAwardEmojiMutation from '../graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
@@ -32,7 +33,7 @@ export function getMutation({ note, name }) {
   };
 }
 
-export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) {
+export function optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid }) {
   const { mutation } = getMutation({ note, name });
 
   const currentUserId = window.gon.current_user_id;
@@ -40,7 +41,7 @@ export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) {
   return (store) => {
     store.updateQuery(
       {
-        query: workItemNotesByIidQuery,
+        query: isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery,
         variables: { fullPath, iid: workItemIid },
       },
       (sourceData) => {
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index 596283a959099..c820c60fe13de 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -64,6 +64,7 @@ describe('Work Item Note Actions', () => {
         projectName,
       },
       provide: {
+        isGroup: false,
         glFeatures: {
           workItemsMvc2: true,
         },
diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
index ce9156359465f..939dd3e870ba3 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -9,6 +9,7 @@ import AwardsList from '~/vue_shared/components/awards_list.vue';
 import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
 import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
 import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
 import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
 import {
   mockWorkItemNotesResponseWithComments,
@@ -45,6 +46,7 @@ describe('Work Item Note Awards List', () => {
   const findAwardsList = () => wrapper.findComponent(AwardsList);
 
   const createComponent = ({
+    isGroup = false,
     note = firstNote,
     addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler,
     removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler,
@@ -55,12 +57,15 @@ describe('Work Item Note Awards List', () => {
     ]);
 
     apolloProvider.clients.defaultClient.writeQuery({
-      query: workItemNotesByIidQuery,
+      query: isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery,
       variables: { fullPath, iid: workItemIid },
       ...mockWorkItemNotesResponseWithComments,
     });
 
     wrapper = shallowMount(WorkItemNoteAwardsList, {
+      provide: {
+        isGroup,
+      },
       propsData: {
         fullPath,
         workItemIid,
@@ -89,17 +94,20 @@ describe('Work Item Note Awards List', () => {
       expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission);
     });
 
-    it('adds award if not already awarded', async () => {
-      createComponent();
-      await waitForPromises();
+    it.each([true, false])(
+      'adds award if not already awarded in both group and project contexts',
+      async (isGroup) => {
+        createComponent({ isGroup });
+        await waitForPromises();
 
-      findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+        findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
 
-      expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
-        awardableId: firstNote.id,
-        name: EMOJI_THUMBSUP,
-      });
-    });
+        expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
+          awardableId: firstNote.id,
+          name: EMOJI_THUMBSUP,
+        });
+      },
+    );
 
     it('emits error if awarding emoji fails', async () => {
       createComponent({
@@ -114,20 +122,23 @@ describe('Work Item Note Awards List', () => {
       expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]);
     });
 
-    it('removes award if already awarded', async () => {
-      const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
+    it.each([true, false])(
+      'removes award if already awarded in both group and project contexts',
+      async (isGroup) => {
+        const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
 
-      createComponent({ removeAwardEmojiMutationHandler });
+        createComponent({ isGroup, removeAwardEmojiMutationHandler });
 
-      findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+        findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
 
-      await waitForPromises();
+        await waitForPromises();
 
-      expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
-        awardableId: firstNote.id,
-        name: EMOJI_THUMBSDOWN,
-      });
-    });
+        expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
+          awardableId: firstNote.id,
+          name: EMOJI_THUMBSDOWN,
+        });
+      },
+    );
 
     it('restores award if remove fails', async () => {
       createComponent({
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 9e02e0708d48b..2620242000e78 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -10,6 +10,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
 import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
 import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
 import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
+import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
 import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
 import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
 import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql';
@@ -63,6 +64,9 @@ describe('WorkItemNotes component', () => {
   const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
   const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
 
+  const groupWorkItemNotesQueryHandler = jest
+    .fn()
+    .mockResolvedValue(mockWorkItemNotesByIidResponse);
   const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesByIidResponse);
   const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
   const workItemNotesWithCommentsQueryHandler = jest
@@ -87,17 +91,22 @@ describe('WorkItemNotes component', () => {
     workItemIid = mockWorkItemIid,
     defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
     deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
+    isGroup = false,
     isModal = false,
     isWorkItemConfidential = false,
   } = {}) => {
     wrapper = shallowMount(WorkItemNotes, {
       apolloProvider: createMockApollo([
         [workItemNotesByIidQuery, defaultWorkItemNotesQueryHandler],
+        [groupWorkItemNotesByIidQuery, groupWorkItemNotesQueryHandler],
         [deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
         [workItemNoteCreatedSubscription, notesCreateSubscriptionHandler],
         [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler],
         [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler],
       ]),
+      provide: {
+        isGroup,
+      },
       propsData: {
         fullPath: 'test-path',
         workItemId,
@@ -354,4 +363,22 @@ describe('WorkItemNotes component', () => {
 
     expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true);
   });
+
+  describe('when project context', () => {
+    it('calls the project work item query', async () => {
+      createComponent();
+      await waitForPromises();
+
+      expect(workItemNotesQueryHandler).toHaveBeenCalled();
+    });
+  });
+
+  describe('when group context', () => {
+    it('calls the group work item query', async () => {
+      createComponent({ isGroup: true });
+      await waitForPromises();
+
+      expect(groupWorkItemNotesQueryHandler).toHaveBeenCalled();
+    });
+  });
 });
diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js
index 8ae32ce5f4081..43eceb13b67c2 100644
--- a/spec/frontend/work_items/notes/award_utils_spec.js
+++ b/spec/frontend/work_items/notes/award_utils_spec.js
@@ -2,6 +2,7 @@ import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_uti
 import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 import mockApollo from 'helpers/mock_apollo_helper';
 import { __ } from '~/locale';
+import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
 import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
 import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
 import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
@@ -105,5 +106,22 @@ describe('Work item note award utils', () => {
 
       expect(updatedNote.awardEmoji.nodes).toEqual([]);
     });
+
+    it.each`
+      description                                      | isGroup  | query
+      ${'calls project query when in project context'} | ${false} | ${workItemNotesByIidQuery}
+      ${'calls group query when in group context'}     | ${true}  | ${groupWorkItemNotesByIidQuery}
+    `('$description', ({ isGroup, query }) => {
+      const note = firstNote;
+      const { name } = mockAwardEmojiThumbsUp;
+      const cacheSpy = { updateQuery: jest.fn() };
+
+      optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid })(cacheSpy);
+
+      expect(cacheSpy.updateQuery).toHaveBeenCalledWith(
+        { query, variables: { fullPath, iid: workItemIid } },
+        expect.any(Function),
+      );
+    });
   });
 });
-- 
GitLab