Commit aad1c72d authored by Rajat Jain's avatar Rajat Jain

Resolve todo when resolve button is clicked

Update Apollo cache for pending todos when a
comment is resolved on design files.

Changelog: fixed
parent a87a3d2c
...@@ -4,13 +4,16 @@ import { ApolloMutation } from 'vue-apollo'; ...@@ -4,13 +4,16 @@ import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql'; import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql'; import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql'; import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import allVersionsMixin from '../../mixins/all_versions'; import allVersionsMixin from '../../mixins/all_versions';
import { hasErrors } from '../../utils/cache_update'; import { hasErrors } from '../../utils/cache_update';
import { extractDesign } from '../../utils/design_management_utils';
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages'; import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
import DesignNote from './design_note.vue'; import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue'; import DesignReplyForm from './design_reply_form.vue';
...@@ -161,6 +164,19 @@ export default { ...@@ -161,6 +164,19 @@ export default {
}, },
toggleResolvedStatus() { toggleResolvedStatus() {
this.isResolving = true; this.isResolving = true;
/**
* Get previous todo count
*/
const { defaultClient: client } = this.$apollo.provider.clients;
const sourceData = client.readQuery({
query: getDesignQuery,
variables: this.designVariables,
});
const design = extractDesign(sourceData);
const prevTodoCount = design.currentUserTodos?.nodes?.length || 0;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: toggleResolveDiscussionMutation, mutation: toggleResolveDiscussionMutation,
...@@ -170,6 +186,10 @@ export default { ...@@ -170,6 +186,10 @@ export default {
if (data.errors?.length > 0) { if (data.errors?.length > 0) {
this.$emit('resolve-discussion-error', data.errors[0]); this.$emit('resolve-discussion-error', data.errors[0]);
} }
const newTodoCount =
data?.discussionToggleResolve?.discussion?.noteable?.currentUserTodos?.nodes?.length ||
0;
updateGlobalTodoCount(newTodoCount - prevTodoCount);
}) })
.catch((err) => { .catch((err) => {
this.$emit('resolve-discussion-error', err); this.$emit('resolve-discussion-error', err);
......
import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; import { defaultDataIdFromObject, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import produce from 'immer'; import produce from 'immer';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import introspectionQueryResultData from './graphql/fragmentTypes.json';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql'; import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
import getDesignQuery from './graphql/queries/get_design.query.graphql'; import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql'; import typeDefs from './graphql/typedefs.graphql';
...@@ -12,6 +13,10 @@ import { addPendingTodoToStore } from './utils/cache_update'; ...@@ -12,6 +13,10 @@ import { addPendingTodoToStore } from './utils/cache_update';
import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils'; import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages'; import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
Vue.use(VueApollo); Vue.use(VueApollo);
const resolvers = { const resolvers = {
...@@ -80,6 +85,7 @@ const defaultClient = createDefaultClient( ...@@ -80,6 +85,7 @@ const defaultClient = createDefaultClient(
} }
return defaultDataIdFromObject(object); return defaultDataIdFromObject(object);
}, },
fragmentMatcher,
}, },
typeDefs, typeDefs,
assumeImmutableResults: true, assumeImmutableResults: true,
......
{"__schema":{"types":[{"kind":"INTERFACE","name":"User","possibleTypes":[{"name":"UserCore"}]},{"kind":"UNION","name":"NoteableType","possibleTypes":[{"name":"Design"},{"name":"Issue"},{"name":"MergeRequest"}]}]}}
fragment DesignTodoItem on Design {
id
image
__typename
currentUserTodos(state: pending) {
nodes {
id
__typename
}
}
}
#import "../fragments/design_note.fragment.graphql" #import "../fragments/design_note.fragment.graphql"
#import "../fragments/design_todo_item.fragment.graphql"
mutation createImageDiffNote($input: CreateImageDiffNoteInput!) { mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
createImageDiffNote(input: $input) { createImageDiffNote(input: $input) {
...@@ -7,6 +8,11 @@ mutation createImageDiffNote($input: CreateImageDiffNoteInput!) { ...@@ -7,6 +8,11 @@ mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
discussion { discussion {
id id
replyId replyId
noteable {
... on Design {
...DesignTodoItem
}
}
notes { notes {
nodes { nodes {
...DesignNote ...DesignNote
......
#import "../fragments/design_note.fragment.graphql" #import "../fragments/design_note.fragment.graphql"
#import "../fragments/discussion_resolved_status.fragment.graphql" #import "../fragments/discussion_resolved_status.fragment.graphql"
#import "../fragments/design_todo_item.fragment.graphql"
mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) { mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
discussionToggleResolve(input: { id: $id, resolve: $resolve }) { discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion { discussion {
id id
...ResolvedStatus ...ResolvedStatus
noteable {
... on Design {
...DesignTodoItem
}
}
notes { notes {
nodes { nodes {
...DesignNote ...DesignNote
......
<script> <script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { isNull } from 'lodash';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings'; import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DesignDestroyer from '../../components/design_destroyer.vue'; import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
...@@ -93,6 +95,7 @@ export default { ...@@ -93,6 +95,7 @@ export default {
errorMessage: '', errorMessage: '',
scale: DEFAULT_SCALE, scale: DEFAULT_SCALE,
resolvedDiscussionsExpanded: false, resolvedDiscussionsExpanded: false,
prevCurrentUserTodos: null,
}; };
}, },
apollo: { apollo: {
...@@ -163,6 +166,13 @@ export default { ...@@ -163,6 +166,13 @@ export default {
resolvedDiscussions() { resolvedDiscussions() {
return this.discussions.filter((discussion) => discussion.resolved); return this.discussions.filter((discussion) => discussion.resolved);
}, },
currentUserTodos() {
if (!this.design || !this.design.currentUserTodos) {
return null;
}
return this.design.currentUserTodos?.nodes?.length;
},
}, },
watch: { watch: {
resolvedDiscussions(val) { resolvedDiscussions(val) {
...@@ -170,6 +180,9 @@ export default { ...@@ -170,6 +180,9 @@ export default {
this.resolvedDiscussionsExpanded = false; this.resolvedDiscussionsExpanded = false;
} }
}, },
currentUserTodos(_, prevCurrentUserTodos) {
this.prevCurrentUserTodos = prevCurrentUserTodos;
},
}, },
mounted() { mounted() {
Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign); Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign);
...@@ -272,9 +285,14 @@ export default { ...@@ -272,9 +285,14 @@ export default {
this.$refs.newDiscussionForm.focusInput(); this.$refs.newDiscussionForm.focusInput();
} }
}, },
closeCommentForm() { closeCommentForm(data) {
this.comment = ''; this.comment = '';
this.annotationCoordinates = null; this.annotationCoordinates = null;
if (data?.data && !isNull(this.prevCurrentUserTodos)) {
updateGlobalTodoCount(this.currentUserTodos - this.prevCurrentUserTodos);
this.prevCurrentUserTodos = this.currentUserTodos;
}
}, },
closeDesign() { closeDesign() {
this.$router.push({ this.$router.push({
......
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { todoLabel } from './utils'; import { todoLabel, updateGlobalTodoCount } from './utils';
export default { export default {
components: { components: {
...@@ -19,23 +19,11 @@ export default { ...@@ -19,23 +19,11 @@ export default {
}, },
}, },
methods: { methods: {
updateGlobalTodoCount(additionalTodoCount) {
const countContainer = document.querySelector('.js-todos-count');
if (countContainer === null) return;
const currentCount = parseInt(countContainer.innerText, 10);
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(currentCount + additionalTodoCount, 0),
},
});
document.dispatchEvent(todoToggleEvent);
},
incrementGlobalTodoCount() { incrementGlobalTodoCount() {
this.updateGlobalTodoCount(1); updateGlobalTodoCount(1);
}, },
decrementGlobalTodoCount() { decrementGlobalTodoCount() {
this.updateGlobalTodoCount(-1); updateGlobalTodoCount(-1);
}, },
onToggle(event) { onToggle(event) {
if (this.isTodo) { if (this.isTodo) {
......
...@@ -3,3 +3,19 @@ import { __ } from '~/locale'; ...@@ -3,3 +3,19 @@ import { __ } from '~/locale';
export const todoLabel = (hasTodo) => { export const todoLabel = (hasTodo) => {
return hasTodo ? __('Mark as done') : __('Add a to do'); return hasTodo ? __('Mark as done') : __('Add a to do');
}; };
export const updateGlobalTodoCount = (additionalTodoCount) => {
const countContainer = document.querySelector('.js-todos-count');
if (countContainer === null) return;
const currentCount = parseInt(countContainer.innerText, 10);
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(currentCount + additionalTodoCount, 0),
},
});
document.dispatchEvent(todoToggleEvent);
};
...@@ -17,6 +17,8 @@ const defaultMockDiscussion = { ...@@ -17,6 +17,8 @@ const defaultMockDiscussion = {
notes, notes,
}; };
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => { describe('Design discussions component', () => {
let wrapper; let wrapper;
...@@ -41,8 +43,14 @@ describe('Design discussions component', () => { ...@@ -41,8 +43,14 @@ describe('Design discussions component', () => {
}, },
}; };
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } }); const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
const readQuery = jest.fn().mockReturnValue({
project: {
issue: { designCollection: { designs: { nodes: [{ currentUserTodos: { nodes: [] } }] } } },
},
});
const $apollo = { const $apollo = {
mutate, mutate,
provider: { clients: { defaultClient: { readQuery } } },
}; };
function createComponent(props = {}, data = {}) { function createComponent(props = {}, data = {}) {
...@@ -69,6 +77,12 @@ describe('Design discussions component', () => { ...@@ -69,6 +77,12 @@ describe('Design discussions component', () => {
$apollo, $apollo,
$route: { $route: {
hash: '#note_1', hash: '#note_1',
params: {
id: 1,
},
query: {
version: null,
},
}, },
}, },
}); });
...@@ -138,7 +152,13 @@ describe('Design discussions component', () => { ...@@ -138,7 +152,13 @@ describe('Design discussions component', () => {
}); });
describe('when discussion is resolved', () => { describe('when discussion is resolved', () => {
let dispatchEventSpy;
beforeEach(() => { beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: DEFAULT_TODO_COUNT,
});
createComponent({ createComponent({
discussion: { discussion: {
...defaultMockDiscussion, ...defaultMockDiscussion,
...@@ -174,6 +194,24 @@ describe('Design discussions component', () => { ...@@ -174,6 +194,24 @@ describe('Design discussions component', () => {
expect(findResolveIcon().props('name')).toBe('check-circle-filled'); expect(findResolveIcon().props('name')).toBe('check-circle-filled');
}); });
it('emit todo:toggle when discussion is resolved', async () => {
createComponent(
{ discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
await mutate();
await wrapper.vm.$nextTick();
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: DEFAULT_TODO_COUNT });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
describe('when replies are expanded', () => { describe('when replies are expanded', () => {
beforeEach(() => { beforeEach(() => {
findRepliesWidget().vm.$emit('toggle'); findRepliesWidget().vm.$emit('toggle');
......
...@@ -172,3 +172,40 @@ export const moveDesignMutationResponseWithErrors = { ...@@ -172,3 +172,40 @@ export const moveDesignMutationResponseWithErrors = {
}, },
}, },
}; };
export const resolveCommentMutationResponse = {
discussionToggleResolve: {
discussion: {
noteable: {
id: 'gid://gitlab/DesignManagement::Design/1',
currentUserTodos: {
nodes: [],
__typename: 'TodoConnection',
},
__typename: 'Design',
},
__typename: 'Discussion',
},
errors: [],
__typename: 'DiscussionToggleResolvePayload',
},
};
export const getDesignQueryResponse = {
project: {
issue: {
designCollection: {
designs: {
nodes: [
{
id: 'gid://gitlab/DesignManagement::Design/1',
currentUserTodos: {
nodes: [{ id: 'gid://gitlab/Todo::1' }],
},
},
],
},
},
},
},
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment