diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue index 4ea88842dc6bdef57ec45944d8d95bf5dc828bca..efa40b647900498c62f3718ecfadcdb4af6c5955 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue @@ -9,7 +9,9 @@ import { clearDraft } from '~/lib/utils/autosave'; import { findWidget } from '~/issues/list/utils'; import DiscussionReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue'; +import { updateCacheAfterCreatingNote } from '../../graphql/cache_utils'; import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql'; +import workItemNotesByIidQuery from '../../graphql/notes/work_item_notes_by_iid.query.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; import { TRACKING_CATEGORY_SHOW, WIDGET_TYPE_EMAIL_PARTICIPANTS, i18n } from '../../constants'; import WorkItemNoteSignedOut from './work_item_note_signed_out.vue'; @@ -276,11 +278,27 @@ export default { this.isEditing = true; this.$emit('startReplying'); }, - onNoteUpdate(store, createNoteData) { - const numErrors = createNoteData.data?.createNote?.errors?.length; + addDiscussionToCache(cache, newNote) { + const queryArgs = { + query: workItemNotesByIidQuery, + variables: { fullPath: this.fullPath, iid: this.workItemIid }, + }; + const sourceData = cache.readQuery(queryArgs); + if (!sourceData) { + return; + } + cache.writeQuery({ + ...queryArgs, + data: updateCacheAfterCreatingNote(sourceData, newNote), + }); + }, + onNoteUpdate(cache, { data }) { + this.addDiscussionToCache(cache, data.createNote.note); + + const numErrors = data?.createNote?.errors?.length; if (numErrors) { - const { errors } = createNoteData.data.createNote; + const { errors } = data.createNote; // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/503600 // Refetching widgets as a temporary solution for dynamic updates @@ -304,7 +322,7 @@ export default { return; } - throw new Error(createNoteData.data?.createNote?.errors[0]); + throw new Error(data?.createNote?.errors[0]); } }, }, diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue index 8082d83c6594c42798b1c407810eb0028b694eee..0a532a397b32f76199395fb6f4790fb51f799f17 100644 --- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue +++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue @@ -6,7 +6,7 @@ import { WORKSPACE_PROJECT } from '~/issues/constants'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; -import { isNotesWidget } from '../utils'; +import { findNotesWidget } from '../utils'; import WorkItemStateBadge from './work_item_state_badge.vue'; import WorkItemTypeIcon from './work_item_type_icon.vue'; @@ -57,7 +57,7 @@ export default { return this.workItem?.workItemType?.iconName; }, isDiscussionLocked() { - return this.workItem?.widgets?.find(isNotesWidget)?.discussionLocked; + return findNotesWidget(this.workItem)?.discussionLocked; }, isWorkItemConfidential() { return this.workItem?.confidential; 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 94127e68a06b967c6c29d0c58aa5e95d8dcab8d6..530a7c905ace981e905629ada3044f4d38386766 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -307,8 +307,8 @@ export default { subscribeToMore: [ { document: workItemNoteCreatedSubscription, - updateQuery(previousResult, { subscriptionData }) { - return updateCacheAfterCreatingNote(previousResult, subscriptionData); + updateQuery(previousResult, { subscriptionData: { data } }) { + return updateCacheAfterCreatingNote(previousResult, data?.workItemNoteCreated); }, variables() { return { diff --git a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue index 283ddef5c1db44e31427a93e9d3817f9afb14d1e..cd0703961a7815ea993b3f1f648f392dee9de345 100644 --- a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue +++ b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue @@ -4,7 +4,7 @@ import LockedBadge from '~/issuable/components/locked_badge.vue'; import { WORKSPACE_PROJECT } from '~/issues/constants'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { isNotesWidget } from '../utils'; +import { findNotesWidget } from '../utils'; import WorkItemActions from './work_item_actions.vue'; import TodosToggle from './shared/todos_toggle.vue'; import WorkItemStateBadge from './work_item_state_badge.vue'; @@ -107,7 +107,7 @@ export default { return this.workItem.userPermissions?.reportSpam; }, isDiscussionLocked() { - return this.workItem.widgets?.find(isNotesWidget)?.discussionLocked; + return findNotesWidget(this.workItem)?.discussionLocked; }, workItemType() { return this.workItem.workItemType?.name; diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js index 000915276918bdefdc77a8ae75d672296b637f8b..7ee9691487ffd640a55434e630bec59c3d326d71 100644 --- a/app/assets/javascripts/work_items/graphql/cache_utils.js +++ b/app/assets/javascripts/work_items/graphql/cache_utils.js @@ -12,10 +12,11 @@ import { findWidget } from '~/issues/list/utils'; import { findHierarchyWidgets, findHierarchyWidgetChildren, + findNotesWidget, + getNewWorkItemAutoSaveKey, isNotesWidget, newWorkItemFullPath, newWorkItemId, - getNewWorkItemAutoSaveKey, } from '../utils'; import { WIDGET_TYPE_ASSIGNEES, @@ -41,8 +42,7 @@ import workItemByIidQuery from './work_item_by_iid.query.graphql'; import workItemByIdQuery from './work_item_by_id.query.graphql'; import getWorkItemTreeQuery from './work_item_tree.query.graphql'; -const getNotesWidgetFromSourceData = (draftData) => - draftData?.workspace?.workItem?.widgets.find(isNotesWidget); +const getNotesWidgetFromSourceData = (draftData) => findNotesWidget(draftData?.workspace?.workItem); const updateNotesWidgetDataInDraftData = (draftData, notesWidget) => { const noteWidgetIndex = draftData.workspace.workItem.widgets.findIndex(isNotesWidget); @@ -53,13 +53,12 @@ const updateNotesWidgetDataInDraftData = (draftData, notesWidget) => { * Work Item note create subscription update query callback * * @param currentNotes - * @param subscriptionData + * @param newNote */ -export const updateCacheAfterCreatingNote = (currentNotes, subscriptionData) => { - if (!subscriptionData.data?.workItemNoteCreated) { +export const updateCacheAfterCreatingNote = (currentNotes, newNote) => { + if (!newNote) { return currentNotes; } - const newNote = subscriptionData.data.workItemNoteCreated; return produce(currentNotes, (draftData) => { const notesWidget = getNotesWidgetFromSourceData(draftData); diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js index 38966ab9df420cdaf6e08cbf43739e3636576b4b..4585453e71eea32061cf5a46c908b1e631815e7e 100644 --- a/app/assets/javascripts/work_items/utils.js +++ b/app/assets/javascripts/work_items/utils.js @@ -48,6 +48,9 @@ export const findHierarchyWidgets = (widgets) => export const findLinkedItemsWidget = (workItem) => workItem.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS); +export const findNotesWidget = (workItem) => + workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_NOTES); + export const findStartAndDueDateWidget = (workItem) => workItem.widgets?.find((widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE); diff --git a/spec/frontend/work_items/graphql/cache_utils_spec.js b/spec/frontend/work_items/graphql/cache_utils_spec.js index e2a2488425b2b5d8b79d462a0b4eb332e7f71644..0d6968f1f3381c3b5b3cb396e97898cb701cea9f 100644 --- a/spec/frontend/work_items/graphql/cache_utils_spec.js +++ b/spec/frontend/work_items/graphql/cache_utils_spec.js @@ -1,18 +1,22 @@ +import { cloneDeep } from 'lodash'; import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants'; import { addHierarchyChild, removeHierarchyChild, addHierarchyChildren, setNewWorkItemCache, + updateCacheAfterCreatingNote, updateCountsForParent, } from '~/work_items/graphql/cache_utils'; -import { findHierarchyWidgets } from '~/work_items/utils'; +import { findHierarchyWidgets, findNotesWidget } from '~/work_items/utils'; import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql'; import waitForPromises from 'helpers/wait_for_promises'; import { apolloProvider } from '~/graphql_shared/issuable_client'; import { - workItemHierarchyResponse, childrenWorkItems, + createWorkItemNoteResponse, + mockWorkItemNotesByIidResponse, + workItemHierarchyResponse, workItemResponseFactory, } from '../mock_data'; @@ -462,6 +466,46 @@ describe('work items graphql cache utils', () => { }); }); + describe('updateCacheAfterCreatingNote', () => { + const findDiscussions = ({ workspace }) => + findNotesWidget(workspace.workItem).discussions.nodes; + + it('adds a new discussion to the notes widget', () => { + const currentNotes = mockWorkItemNotesByIidResponse.data; + const newNote = createWorkItemNoteResponse().data.createNote.note; + + expect(findDiscussions(currentNotes)).toHaveLength(3); + + const updatedNotes = updateCacheAfterCreatingNote(currentNotes, newNote); + + expect(findDiscussions(updatedNotes)).toHaveLength(4); + expect(findDiscussions(updatedNotes).at(-1)).toBe(newNote.discussion); + }); + + it('does not modify notes widget when newNote is undefined', () => { + const currentNotes = mockWorkItemNotesByIidResponse.data; + const newNote = undefined; + + expect(findDiscussions(currentNotes)).toHaveLength(3); + + const updatedNotes = updateCacheAfterCreatingNote(currentNotes, newNote); + + expect(findDiscussions(updatedNotes)).toHaveLength(3); + }); + + it('does not add duplicate discussions', () => { + const currentNotes = cloneDeep(mockWorkItemNotesByIidResponse.data); + const newNote = createWorkItemNoteResponse().data.createNote.note; + findDiscussions(currentNotes).push(newNote.discussion); + + expect(findDiscussions(currentNotes)).toHaveLength(4); + + const updatedNotes = updateCacheAfterCreatingNote(currentNotes, newNote); + + expect(findDiscussions(updatedNotes)).toHaveLength(4); + }); + }); + describe('updateCountsForParent', () => { const mockWorkItemData = workItemResponseFactory(); const mockCache = {