diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/shared/todos_toggle.vue similarity index 71% rename from app/assets/javascripts/work_items/components/work_item_todos.vue rename to app/assets/javascripts/work_items/components/shared/todos_toggle.vue index 471dc33cbe29a35148c7b938e714a8038a008bb0..0ce2fc77a0f6e153ead531d6e033765cce289623 100644 --- a/app/assets/javascripts/work_items/components/work_item_todos.vue +++ b/app/assets/javascripts/work_items/components/shared/todos_toggle.vue @@ -1,20 +1,17 @@ <script> import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import { produce } from 'immer'; import { s__ } from '~/locale'; import { updateGlobalTodoCount } from '~/sidebar/utils'; -import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; -import createWorkItemTodosMutation from '../graphql/create_work_item_todos.mutation.graphql'; -import markDoneWorkItemTodosMutation from '../graphql/mark_done_work_item_todos.mutation.graphql'; +import createWorkItemTodosMutation from '../../graphql/create_work_item_todos.mutation.graphql'; +import markDoneWorkItemTodosMutation from '../../graphql/mark_done_work_item_todos.mutation.graphql'; import { TODO_ADD_ICON, TODO_DONE_ICON, TODO_PENDING_STATE, TODO_DONE_STATE, - WIDGET_TYPE_CURRENT_USER_TODOS, -} from '../constants'; +} from '../../constants'; export default { i18n: { @@ -29,15 +26,7 @@ export default { GlButton, }, props: { - workItemId: { - type: String, - required: true, - }, - workItemIid: { - type: String, - required: true, - }, - workItemFullpath: { + itemId: { type: String, required: true, }, @@ -46,6 +35,11 @@ export default { required: false, default: () => [], }, + todosButtonType: { + type: String, + required: false, + default: 'tertiary', + }, }, data() { return { @@ -73,7 +67,7 @@ export default { this.buttonLabel = ''; let mutation = createWorkItemTodosMutation; let inputVariables = { - targetId: this.workItemId, + targetId: this.itemId, }; if (this.pendingTodo) { mutation = markDoneWorkItemTodosMutation; @@ -114,11 +108,7 @@ export default { id: todo.id, }); } - - this.updateWorkItemCurrentTodosWidgetCache({ - cache, - todos, - }); + this.$emit('todosUpdated', { cache, todos }); }, }) .then( @@ -146,26 +136,6 @@ export default { this.isLoading = false; }); }, - updateWorkItemCurrentTodosWidgetCache({ cache, todos }) { - const query = { - query: workItemByIidQuery, - variables: { fullPath: this.workItemFullpath, iid: this.workItemIid }, - }; - - const sourceData = cache.readQuery(query); - - const newData = produce(sourceData, (draftState) => { - const { widgets } = draftState.workspace.workItem; - - const widgetCurrentUserTodos = widgets.find( - (widget) => widget.type === WIDGET_TYPE_CURRENT_USER_TODOS, - ); - - widgetCurrentUserTodos.currentUserTodos.nodes = todos; - }); - - cache.writeQuery({ ...query, data: newData }); - }, }, }; </script> @@ -175,7 +145,7 @@ export default { v-gl-tooltip.hover :disabled="isLoading" :title="buttonLabel" - category="secondary" + :category="todosButtonType" class="btn-icon" :aria-label="buttonLabel" @click="onToggle" diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 939e3d6defb5a8cbc54e1a5c844dcafeaccccc88..0cb008de6f9a38c936ea28ecbf8382e9f2dac175 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -37,6 +37,7 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; import getAllowedWorkItemChildTypes from '../graphql/work_item_allowed_children.query.graphql'; import workspacePermissionsQuery from '../graphql/workspace_permissions.query.graphql'; import { findHierarchyWidgetDefinition } from '../utils'; +import { updateWorkItemCurrentTodosWidget } from '../graphql/cache_utils'; import getWorkItemDesignListQuery from './design_management/graphql/design_collection.query.graphql'; import uploadDesignMutation from './design_management/graphql/upload_design.mutation.graphql'; @@ -51,7 +52,7 @@ import { import WorkItemTree from './work_item_links/work_item_tree.vue'; import WorkItemActions from './work_item_actions.vue'; -import WorkItemTodos from './work_item_todos.vue'; +import TodosToggle from './shared/todos_toggle.vue'; import WorkItemNotificationsWidget from './work_item_notifications_widget.vue'; import WorkItemAttributesWrapper from './work_item_attributes_wrapper.vue'; import WorkItemCreatedUpdated from './work_item_created_updated.vue'; @@ -86,7 +87,7 @@ export default { GlButton, GlEmptyState, WorkItemActions, - WorkItemTodos, + TodosToggle, WorkItemNotificationsWidget, WorkItemCreatedUpdated, WorkItemDescription, @@ -628,6 +629,14 @@ export default { this.resetFilesToBeSaved(); this.designUploadError = UPLOAD_DESIGN_ERROR_MESSAGE; }, + updateWorkItemCurrentTodosWidgetCache({ cache, todos }) { + updateWorkItemCurrentTodosWidget({ + cache, + todos, + fullPath: this.workItemFullPath, + iid: this.workItemIid, + }); + }, }, WORK_ITEM_TYPE_VALUE_OBJECTIVE, WORKSPACE_PROJECT, @@ -658,6 +667,7 @@ export default { @toggleEditMode="enableEditMode" @workItemStateUpdated="$emit('workItemStateUpdated')" @toggleReportAbuseModal="toggleReportAbuseModal" + @todosUpdated="updateWorkItemCurrentTodosWidgetCache" /> <section class="work-item-view"> <section v-if="updateError" class="flash-container flash-container-page sticky"> @@ -708,12 +718,12 @@ export default { > {{ __('Edit') }} </gl-button> - <work-item-todos + <todos-toggle v-if="showWorkItemCurrentUserTodos" - :work-item-id="workItem.id" - :work-item-iid="workItemIid" - :work-item-fullpath="workItemFullPath" + :item-id="workItem.id" :current-user-todos="currentUserTodos" + :todos-button-type="'secondary'" + @todosUpdated="updateWorkItemCurrentTodosWidgetCache" @error="updateError = $event" /> <work-item-notifications-widget 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 5dc2122caffdb4c59a9aa14b435cfd1afd037b83..11d645c1f493bb9b7c2400b09afdfcdc9c8affca 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 @@ -6,7 +6,7 @@ import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge. import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { isNotesWidget } from '../utils'; import WorkItemActions from './work_item_actions.vue'; -import WorkItemTodos from './work_item_todos.vue'; +import TodosToggle from './shared/todos_toggle.vue'; import WorkItemStateBadge from './work_item_state_badge.vue'; import WorkItemNotificationsWidget from './work_item_notifications_widget.vue'; @@ -16,7 +16,7 @@ export default { GlIntersectionObserver, GlLoadingIcon, WorkItemActions, - WorkItemTodos, + TodosToggle, ConfidentialityBadge, WorkItemStateBadge, WorkItemNotificationsWidget, @@ -141,13 +141,13 @@ export default { > {{ __('Edit') }} </gl-button> - <work-item-todos + <todos-toggle v-if="showWorkItemCurrentUserTodos" - :work-item-id="workItem.id" - :work-item-iid="workItem.iid" - :work-item-fullpath="projectFullPath" + :item-id="workItem.id" :current-user-todos="currentUserTodos" - @error="$emit('error')" + :todos-button-type="'secondary'" + @todosUpdated="$emit('todosUpdated', $event)" + @error="updateError = $event" /> <work-item-notifications-widget v-if="newTodoAndNotificationsEnabled" diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js index e6a73c677af8efd1289893cf79ba1f4ce81df9b1..5ccc5cc3480560d07591a40c66c498fa92d1a9b8 100644 --- a/app/assets/javascripts/work_items/graphql/cache_utils.js +++ b/app/assets/javascripts/work_items/graphql/cache_utils.js @@ -33,6 +33,7 @@ import { WIDGET_TYPE_DESCRIPTION, WIDGET_TYPE_CRM_CONTACTS, NEW_WORK_ITEM_IID, + WIDGET_TYPE_CURRENT_USER_TODOS, } from '../constants'; import workItemByIidQuery from './work_item_by_iid.query.graphql'; import getWorkItemTreeQuery from './work_item_tree.query.graphql'; @@ -266,6 +267,30 @@ export const updateParent = ({ cache, fullPath, iid, workItem }) => { }); }; +export const updateWorkItemCurrentTodosWidget = ({ cache, fullPath, iid, todos }) => { + const query = { + query: workItemByIidQuery, + variables: { fullPath, iid }, + }; + + const sourceData = cache.readQuery(query); + + if (!sourceData) { + return; + } + + const newData = produce(sourceData, (draftState) => { + const { widgets } = draftState.workspace.workItem; + const widgetCurrentUserTodos = widgets.find( + (widget) => widget.type === WIDGET_TYPE_CURRENT_USER_TODOS, + ); + + widgetCurrentUserTodos.currentUserTodos.nodes = todos; + }); + + cache.writeQuery({ ...query, data: newData }); +}; + export const setNewWorkItemCache = async ( fullPath, widgetDefinitions, diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/shared/todos_toggle_spec.js similarity index 79% rename from spec/frontend/work_items/components/work_item_todos_spec.js rename to spec/frontend/work_items/components/shared/todos_toggle_spec.js index 90de213a5d176c68507ba87006fd30858502855e..4ec0ec6ae4d2da65b5dfbd836f1d2df7c1839c65 100644 --- a/spec/frontend/work_items/components/work_item_todos_spec.js +++ b/spec/frontend/work_items/components/shared/todos_toggle_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; +import TodosToggle from '~/work_items/components/shared/todos_toggle.vue'; import { TODO_DONE_ICON, TODO_ADD_ICON, @@ -16,9 +16,8 @@ import { import { updateGlobalTodoCount } from '~/sidebar/utils'; import createWorkItemTodosMutation from '~/work_items/graphql/create_work_item_todos.mutation.graphql'; import markDoneWorkItemTodosMutation from '~/work_items/graphql/mark_done_work_item_todos.mutation.graphql'; -import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; -import { workItemResponseFactory, getTodosMutationResponse } from '../mock_data'; +import { workItemResponseFactory, getTodosMutationResponse } from '../../mock_data'; jest.mock('~/sidebar/utils'); @@ -33,8 +32,6 @@ describe('WorkItemTodo component', () => { const errorMessage = 'Failed to add item'; const workItemQueryResponse = workItemResponseFactory({ canUpdate: true }); const mockWorkItemId = workItemQueryResponse.data.workItem.id; - const mockWorkItemIid = workItemQueryResponse.data.workItem.iid; - const mockWorkItemFullpath = workItemQueryResponse.data.workItem.namespace.fullPath; const createTodoSuccessHandler = jest .fn() @@ -60,29 +57,16 @@ describe('WorkItemTodo component', () => { mutation = createWorkItemTodosMutation, currentUserTodosHandler = createTodoSuccessHandler, currentUserTodos = [], + todosButtonType = 'tertiary', } = {}) => { const mockApolloProvider = createMockApollo([[mutation, currentUserTodosHandler]]); - mockApolloProvider.clients.defaultClient.cache.writeQuery({ - query: workItemByIidQuery, - variables: { fullPath: mockWorkItemFullpath, iid: mockWorkItemIid }, - data: { - ...workItemQueryResponse.data, - workspace: { - __typename: 'Project', - id: 'gid://gitlab/Project/1', - workItem: workItemQueryResponse.data.workItem, - }, - }, - }); - - wrapper = shallowMountExtended(WorkItemTodos, { + wrapper = shallowMountExtended(TodosToggle, { apolloProvider: mockApolloProvider, propsData: { - workItemId: mockWorkItemId, - workItemIid: mockWorkItemIid, - workItemFullpath: mockWorkItemFullpath, + itemId: mockWorkItemId, currentUserTodos, + todosButtonType, }, }); }; @@ -93,6 +77,7 @@ describe('WorkItemTodo component', () => { expect(findTodoWidget().exists()).toBe(true); expect(findTodoIcon().props('name')).toEqual(TODO_ADD_ICON); expect(findTodoIcon().classes('!gl-fill-blue-500')).toBe(false); + expect(findTodoWidget().props('category')).toBe('tertiary'); }); it('renders mark as done button when there is pending item', () => { @@ -105,12 +90,12 @@ describe('WorkItemTodo component', () => { }); it.each` - assertionName | mutation | currentUserTodosHandler | currentUserTodos | inputVariables - ${'create'} | ${createWorkItemTodosMutation} | ${createTodoSuccessHandler} | ${[]} | ${inputVariablesCreateTodos} - ${'mark done'} | ${markDoneWorkItemTodosMutation} | ${markDoneTodoSuccessHandler} | ${[mockCurrentUserTodos]} | ${inputVariablesMarkDoneTodos} + assertionName | mutation | currentUserTodosHandler | currentUserTodos | inputVariables | todos + ${'create'} | ${createWorkItemTodosMutation} | ${createTodoSuccessHandler} | ${[]} | ${inputVariablesCreateTodos} | ${[{ id: expect.anything() }]} + ${'mark done'} | ${markDoneWorkItemTodosMutation} | ${markDoneTodoSuccessHandler} | ${[mockCurrentUserTodos]} | ${inputVariablesMarkDoneTodos} | ${[]} `( 'calls $assertionName todos mutation when to do button is toggled', - async ({ mutation, currentUserTodosHandler, currentUserTodos, inputVariables }) => { + async ({ mutation, currentUserTodosHandler, currentUserTodos, inputVariables, todos }) => { createComponent({ mutation, currentUserTodosHandler, @@ -124,10 +109,22 @@ describe('WorkItemTodo component', () => { expect(currentUserTodosHandler).toHaveBeenCalledWith({ input: inputVariables, }); + expect(wrapper.emitted('todosUpdated')[0][0]).toMatchObject({ + cache: expect.anything(), + todos, + }); expect(updateGlobalTodoCount).toHaveBeenCalled(); }, ); + it('renders secondary button when `todosButtonType` is secondary', () => { + createComponent({ + todosButtonType: 'secondary', + }); + + expect(findTodoWidget().props('category')).toBe('secondary'); + }); + it('emits error when the update mutation fails', async () => { createComponent({ currentUserTodosHandler: failureHandler, diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index d1c11ed05c845cb9a086a1abe1ca7f84f6f338f7..d368664cb53dd9921a8969a0e091bdcb8b65a4ce 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -21,7 +21,7 @@ import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal. import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue'; -import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; +import TodosToggle from '~/work_items/components/shared/todos_toggle.vue'; import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue'; import DesignUploadButton from '~/work_items/components//design_management/upload_button.vue'; import uploadDesignMutation from '~/work_items/components/design_management/graphql/upload_design.mutation.graphql'; @@ -113,7 +113,7 @@ describe('WorkItemDetail component', () => { const findNotesWidget = () => wrapper.findComponent(WorkItemNotes); const findModal = () => wrapper.findComponent(WorkItemDetailModal); const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal); - const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos); + const findTodosToggle = () => wrapper.findComponent(TodosToggle); const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader); const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview'); const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar'); @@ -245,7 +245,7 @@ describe('WorkItemDetail component', () => { }); it('renders todos widget if logged in', () => { - expect(findWorkItemTodos().exists()).toBe(true); + expect(findTodosToggle().exists()).toBe(true); }); it('calls the work item updated subscription', () => { @@ -816,7 +816,7 @@ describe('WorkItemDetail component', () => { }); it('does not renders if not logged in', () => { - expect(findWorkItemTodos().exists()).toBe(false); + expect(findTodosToggle().exists()).toBe(false); }); }); diff --git a/spec/frontend/work_items/components/work_item_sticky_header_spec.js b/spec/frontend/work_items/components/work_item_sticky_header_spec.js index 07a2204d1f665263b682dfc1c0a83415d0bfd8c9..19af66ac1f78e47bcfcbaba64aa491cc6db1687d 100644 --- a/spec/frontend/work_items/components/work_item_sticky_header_spec.js +++ b/spec/frontend/work_items/components/work_item_sticky_header_spec.js @@ -7,7 +7,7 @@ import LockedBadge from '~/issuable/components/locked_badge.vue'; import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; -import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; +import TodosToggle from '~/work_items/components/shared/todos_toggle.vue'; import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue'; import WorkItemNotificationsWidget from '~/work_items/components/work_item_notifications_widget.vue'; @@ -46,7 +46,7 @@ describe('WorkItemStickyHeader', () => { const findConfidentialityBadge = () => wrapper.findComponent(ConfidentialityBadge); const findLockedBadge = () => wrapper.findComponent(LockedBadge); const findWorkItemActions = () => wrapper.findComponent(WorkItemActions); - const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos); + const findTodosToggle = () => wrapper.findComponent(TodosToggle); const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); const findWorkItemStateBadge = () => wrapper.findComponent(WorkItemStateBadge); const findEditButton = () => wrapper.findByTestId('work-item-edit-button-sticky'); @@ -67,7 +67,7 @@ describe('WorkItemStickyHeader', () => { createComponent(); expect(findWorkItemTitle().exists()).toBe(true); - expect(findWorkItemTodos().exists()).toBe(true); + expect(findTodosToggle().exists()).toBe(true); expect(findWorkItemActions().exists()).toBe(true); });