Commit a263d43e authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'rj/resolve-design-comment' into 'master'

Resolve todo when resolve button is clicked

See merge request gitlab-org/gitlab!67026
parents 1c23292f aad1c72d
......@@ -4,13 +4,16 @@ import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
import { s__ } from '~/locale';
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 { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.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 { hasErrors } from '../../utils/cache_update';
import { extractDesign } from '../../utils/design_management_utils';
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
......@@ -161,6 +164,19 @@ export default {
},
toggleResolvedStatus() {
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
.mutate({
mutation: toggleResolveDiscussionMutation,
......@@ -170,6 +186,10 @@ export default {
if (data.errors?.length > 0) {
this.$emit('resolve-discussion-error', data.errors[0]);
}
const newTodoCount =
data?.discussionToggleResolve?.discussion?.noteable?.currentUserTodos?.nodes?.length ||
0;
updateGlobalTodoCount(newTodoCount - prevTodoCount);
})
.catch((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 { uniqueId } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import introspectionQueryResultData from './graphql/fragmentTypes.json';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
......@@ -12,6 +13,10 @@ import { addPendingTodoToStore } from './utils/cache_update';
import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
Vue.use(VueApollo);
const resolvers = {
......@@ -80,6 +85,7 @@ const defaultClient = createDefaultClient(
}
return defaultDataIdFromObject(object);
},
fragmentMatcher,
},
typeDefs,
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_todo_item.fragment.graphql"
mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
createImageDiffNote(input: $input) {
......@@ -7,6 +8,11 @@ mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
discussion {
id
replyId
noteable {
... on Design {
...DesignTodoItem
}
}
notes {
nodes {
...DesignNote
......
#import "../fragments/design_note.fragment.graphql"
#import "../fragments/discussion_resolved_status.fragment.graphql"
#import "../fragments/design_todo_item.fragment.graphql"
mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion {
id
...ResolvedStatus
noteable {
... on Design {
...DesignTodoItem
}
}
notes {
nodes {
...DesignNote
......
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { isNull } from 'lodash';
import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import createFlash from '~/flash';
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 DesignDestroyer from '../../components/design_destroyer.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
......@@ -93,6 +95,7 @@ export default {
errorMessage: '',
scale: DEFAULT_SCALE,
resolvedDiscussionsExpanded: false,
prevCurrentUserTodos: null,
};
},
apollo: {
......@@ -163,6 +166,13 @@ export default {
resolvedDiscussions() {
return this.discussions.filter((discussion) => discussion.resolved);
},
currentUserTodos() {
if (!this.design || !this.design.currentUserTodos) {
return null;
}
return this.design.currentUserTodos?.nodes?.length;
},
},
watch: {
resolvedDiscussions(val) {
......@@ -170,6 +180,9 @@ export default {
this.resolvedDiscussionsExpanded = false;
}
},
currentUserTodos(_, prevCurrentUserTodos) {
this.prevCurrentUserTodos = prevCurrentUserTodos;
},
},
mounted() {
Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign);
......@@ -272,9 +285,14 @@ export default {
this.$refs.newDiscussionForm.focusInput();
}
},
closeCommentForm() {
closeCommentForm(data) {
this.comment = '';
this.annotationCoordinates = null;
if (data?.data && !isNull(this.prevCurrentUserTodos)) {
updateGlobalTodoCount(this.currentUserTodos - this.prevCurrentUserTodos);
this.prevCurrentUserTodos = this.currentUserTodos;
}
},
closeDesign() {
this.$router.push({
......
<script>
import { GlButton } from '@gitlab/ui';
import { todoLabel } from './utils';
import { todoLabel, updateGlobalTodoCount } from './utils';
export default {
components: {
......@@ -19,23 +19,11 @@ export default {
},
},
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() {
this.updateGlobalTodoCount(1);
updateGlobalTodoCount(1);
},
decrementGlobalTodoCount() {
this.updateGlobalTodoCount(-1);
updateGlobalTodoCount(-1);
},
onToggle(event) {
if (this.isTodo) {
......
......@@ -3,3 +3,19 @@ import { __ } from '~/locale';
export const todoLabel = (hasTodo) => {
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 = {
notes,
};
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
let wrapper;
......@@ -41,8 +43,14 @@ describe('Design discussions component', () => {
},
};
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
const readQuery = jest.fn().mockReturnValue({
project: {
issue: { designCollection: { designs: { nodes: [{ currentUserTodos: { nodes: [] } }] } } },
},
});
const $apollo = {
mutate,
provider: { clients: { defaultClient: { readQuery } } },
};
function createComponent(props = {}, data = {}) {
......@@ -69,6 +77,12 @@ describe('Design discussions component', () => {
$apollo,
$route: {
hash: '#note_1',
params: {
id: 1,
},
query: {
version: null,
},
},
},
});
......@@ -138,7 +152,13 @@ describe('Design discussions component', () => {
});
describe('when discussion is resolved', () => {
let dispatchEventSpy;
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: DEFAULT_TODO_COUNT,
});
createComponent({
discussion: {
...defaultMockDiscussion,
......@@ -174,6 +194,24 @@ describe('Design discussions component', () => {
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', () => {
beforeEach(() => {
findRepliesWidget().vm.$emit('toggle');
......
......@@ -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