Commit 6be04431 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'fix-scroll-to-note-designs' into 'master'

Fix scroll to note designs

See merge request gitlab-org/gitlab!41062
parents c4fdb208 d6189a13
...@@ -105,8 +105,8 @@ export default { ...@@ -105,8 +105,8 @@ export default {
atVersion: this.designsVersion, atVersion: this.designsVersion,
}; };
}, },
isDiscussionHighlighted() { isDiscussionActive() {
return this.discussion.notes[0].id === this.activeDiscussion.id; return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
}, },
resolveCheckboxText() { resolveCheckboxText() {
return this.discussion.resolved return this.discussion.resolved
...@@ -134,18 +134,6 @@ export default { ...@@ -134,18 +134,6 @@ export default {
isFormVisible() { isFormVisible() {
return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id; return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
}, },
shouldScrollToDiscussion(activeDiscussion) {
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
];
const { id: activeDiscussionId, source: activeDiscussionSource } = activeDiscussion;
return (
ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(activeDiscussionSource) &&
activeDiscussionId === this.discussion.notes[0].id
);
},
}, },
methods: { methods: {
addDiscussionComment( addDiscussionComment(
...@@ -199,6 +187,14 @@ export default { ...@@ -199,6 +187,14 @@ export default {
this.isResolving = false; this.isResolving = false;
}); });
}, },
shouldScrollToDiscussion(activeDiscussion) {
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
];
const { source } = activeDiscussion;
return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
},
}, },
createNoteMutation, createNoteMutation,
}; };
...@@ -221,7 +217,7 @@ export default { ...@@ -221,7 +217,7 @@ export default {
:note="firstNote" :note="firstNote"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving" :is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" :class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('updateNoteError', $event)" @error="$emit('updateNoteError', $event)"
> >
<template v-if="discussion.resolvable" #resolveDiscussion> <template v-if="discussion.resolvable" #resolveDiscussion>
...@@ -265,7 +261,7 @@ export default { ...@@ -265,7 +261,7 @@ export default {
:note="note" :note="note"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving" :is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" :class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('updateNoteError', $event)" @error="$emit('updateNoteError', $event)"
/> />
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> <li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
......
...@@ -7,7 +7,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link ...@@ -7,7 +7,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignReplyForm from './design_reply_form.vue'; import DesignReplyForm from './design_reply_form.vue';
import { findNoteId } from '../../utils/design_management_utils'; import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
import { hasErrors } from '../../utils/cache_update'; import { hasErrors } from '../../utils/cache_update';
export default { export default {
...@@ -47,7 +47,7 @@ export default { ...@@ -47,7 +47,7 @@ export default {
return findNoteId(this.note.id); return findNoteId(this.note.id);
}, },
isNoteLinked() { isNoteLinked() {
return this.$route.hash === `#note_${this.noteAnchorId}`; return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
}, },
mutationPayload() { mutationPayload() {
return { return {
...@@ -59,13 +59,6 @@ export default { ...@@ -59,13 +59,6 @@ export default {
return !this.isEditing && this.note.userPermissions.adminNote; return !this.isEditing && this.note.userPermissions.adminNote;
}, },
}, },
mounted() {
this.$nextTick(() => {
if (this.isNoteLinked) {
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
}
});
},
methods: { methods: {
hideForm() { hideForm() {
this.isEditing = false; this.isEditing = false;
......
...@@ -237,7 +237,12 @@ export default { ...@@ -237,7 +237,12 @@ export default {
}); });
}, },
isNoteInactive(note) { isNoteInactive(note) {
return this.activeDiscussion.id && this.activeDiscussion.id !== note.id; const discussionNotes = note.discussion.notes.nodes || [];
return (
this.activeDiscussion.id &&
!discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
);
}, },
designPinClass(note) { designPinClass(note) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved }; return { inactive: this.isNoteInactive(note), resolved: note.resolved };
......
...@@ -25,5 +25,10 @@ fragment DesignNote on Note { ...@@ -25,5 +25,10 @@ fragment DesignNote on Note {
} }
discussion { discussion {
id id
notes {
nodes {
id
}
}
} }
} }
---
title: Highlight design discussion if any comment in discussion is linked
merge_request: 41062
author:
type: fixed
...@@ -8,8 +8,9 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not ...@@ -8,8 +8,9 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql'; import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue'; import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import mockDiscussion from '../../mock_data/discussion';
const discussion = { const defaultMockDiscussion = {
id: '0', id: '0',
resolved: false, resolved: false,
resolvable: true, resolvable: true,
...@@ -49,7 +50,7 @@ describe('Design discussions component', () => { ...@@ -49,7 +50,7 @@ describe('Design discussions component', () => {
wrapper = mount(DesignDiscussion, { wrapper = mount(DesignDiscussion, {
propsData: { propsData: {
resolvedDiscussionsExpanded: true, resolvedDiscussionsExpanded: true,
discussion, discussion: defaultMockDiscussion,
noteableId: 'noteable-id', noteableId: 'noteable-id',
designId: 'design-id', designId: 'design-id',
discussionIndex: 1, discussionIndex: 1,
...@@ -82,7 +83,7 @@ describe('Design discussions component', () => { ...@@ -82,7 +83,7 @@ describe('Design discussions component', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
discussion: { discussion: {
...discussion, ...defaultMockDiscussion,
resolvable: false, resolvable: false,
}, },
}); });
...@@ -125,7 +126,7 @@ describe('Design discussions component', () => { ...@@ -125,7 +126,7 @@ describe('Design discussions component', () => {
it('renders a checkbox with Resolve thread text in reply form', () => { it('renders a checkbox with Resolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onClick'); findReplyPlaceholder().vm.$emit('onClick');
wrapper.setProps({ discussionWithOpenForm: discussion.id }); wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Resolve thread'); expect(findResolveCheckbox().text()).toBe('Resolve thread');
...@@ -141,7 +142,7 @@ describe('Design discussions component', () => { ...@@ -141,7 +142,7 @@ describe('Design discussions component', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
discussion: { discussion: {
...discussion, ...defaultMockDiscussion,
resolved: true, resolved: true,
resolvedBy: notes[0].author, resolvedBy: notes[0].author,
resolvedAt: '2020-05-08T07:10:45Z', resolvedAt: '2020-05-08T07:10:45Z',
...@@ -206,7 +207,7 @@ describe('Design discussions component', () => { ...@@ -206,7 +207,7 @@ describe('Design discussions component', () => {
it('renders a checkbox with Unresolve thread text in reply form', () => { it('renders a checkbox with Unresolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onClick'); findReplyPlaceholder().vm.$emit('onClick');
wrapper.setProps({ discussionWithOpenForm: discussion.id }); wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Unresolve thread'); expect(findResolveCheckbox().text()).toBe('Unresolve thread');
...@@ -218,7 +219,7 @@ describe('Design discussions component', () => { ...@@ -218,7 +219,7 @@ describe('Design discussions component', () => {
it('hides reply placeholder and opens form on placeholder click', () => { it('hides reply placeholder and opens form on placeholder click', () => {
createComponent(); createComponent();
findReplyPlaceholder().vm.$emit('onClick'); findReplyPlaceholder().vm.$emit('onClick');
wrapper.setProps({ discussionWithOpenForm: discussion.id }); wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findReplyPlaceholder().exists()).toBe(false); expect(findReplyPlaceholder().exists()).toBe(false);
...@@ -228,7 +229,7 @@ describe('Design discussions component', () => { ...@@ -228,7 +229,7 @@ describe('Design discussions component', () => {
it('calls mutation on submitting form and closes the form', () => { it('calls mutation on submitting form and closes the form', () => {
createComponent( createComponent(
{ discussionWithOpenForm: discussion.id }, { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true }, { discussionComment: 'test', isFormRendered: true },
); );
...@@ -246,7 +247,7 @@ describe('Design discussions component', () => { ...@@ -246,7 +247,7 @@ describe('Design discussions component', () => {
it('clears the discussion comment on closing comment form', () => { it('clears the discussion comment on closing comment form', () => {
createComponent( createComponent(
{ discussionWithOpenForm: discussion.id }, { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true }, { discussionComment: 'test', isFormRendered: true },
); );
...@@ -263,19 +264,26 @@ describe('Design discussions component', () => { ...@@ -263,19 +264,26 @@ describe('Design discussions component', () => {
}); });
}); });
it('applies correct class to design notes when discussion is highlighted', () => { describe('when any note from a discussion is active', () => {
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
'applies correct class to all notes in the active discussion',
note => {
createComponent( createComponent(
{}, { discussion: mockDiscussion },
{ {
activeDiscussion: { activeDiscussion: {
id: notes[0].id, id: note.id,
source: 'pin', source: 'pin',
}, },
}, },
); );
expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe( expect(
true, wrapper
.findAll(DesignNote)
.wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
).toBe(true);
},
); );
}); });
...@@ -285,7 +293,7 @@ describe('Design discussions component', () => { ...@@ -285,7 +293,7 @@ describe('Design discussions component', () => {
expect(mutate).toHaveBeenCalledWith({ expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation, mutation: toggleResolveDiscussionMutation,
variables: { variables: {
id: discussion.id, id: defaultMockDiscussion.id,
resolve: true, resolve: true,
}, },
}); });
...@@ -296,7 +304,7 @@ describe('Design discussions component', () => { ...@@ -296,7 +304,7 @@ describe('Design discussions component', () => {
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => { it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
createComponent( createComponent(
{ discussionWithOpenForm: discussion.id }, { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true }, { discussionComment: 'test', isFormRendered: true },
); );
findResolveButton().trigger('click'); findResolveButton().trigger('click');
...@@ -306,7 +314,7 @@ describe('Design discussions component', () => { ...@@ -306,7 +314,7 @@ describe('Design discussions component', () => {
expect(mutate).toHaveBeenCalledWith({ expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation, mutation: toggleResolveDiscussionMutation,
variables: { variables: {
id: discussion.id, id: defaultMockDiscussion.id,
resolve: true, resolve: true,
}, },
}); });
......
...@@ -86,16 +86,6 @@ describe('Design note component', () => { ...@@ -86,16 +86,6 @@ describe('Design note component', () => {
expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true); expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
}); });
it('should trigger a scrollIntoView method', () => {
createComponent({
note,
});
return wrapper.vm.$nextTick().then(() => {
expect(scrollIntoViewMock).toHaveBeenCalled();
});
});
it('should not render edit icon when user does not have a permission', () => { it('should not render edit icon when user does not have a permission', () => {
createComponent({ createComponent({
note, note,
......
...@@ -13,8 +13,9 @@ describe('Design overlay component', () => { ...@@ -13,8 +13,9 @@ describe('Design overlay component', () => {
const findAllNotes = () => wrapper.findAll('.js-image-badge'); const findAllNotes = () => wrapper.findAll('.js-image-badge');
const findCommentBadge = () => wrapper.find('.comment-indicator'); const findCommentBadge = () => wrapper.find('.comment-indicator');
const findFirstBadge = () => findAllNotes().at(0); const findBadgeAtIndex = noteIndex => findAllNotes().at(noteIndex);
const findSecondBadge = () => findAllNotes().at(1); const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
const clickAndDragBadge = (elem, fromPoint, toPoint) => { const clickAndDragBadge = (elem, fromPoint, toPoint) => {
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y }); elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
...@@ -104,7 +105,32 @@ describe('Design overlay component', () => { ...@@ -104,7 +105,32 @@ describe('Design overlay component', () => {
expect(findSecondBadge().classes()).toContain('resolved'); expect(findSecondBadge().classes()).toContain('resolved');
}); });
it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => { describe('when no discussion is active', () => {
it('should not apply inactive class to any pins', () => {
expect(
findAllNotes(0).wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
).toBe(false);
});
});
describe('when a discussion is active', () => {
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
'should not apply inactive class to the pin for the active discussion',
note => {
wrapper.setData({
activeDiscussion: {
id: note.id,
source: 'discussion',
},
});
return wrapper.vm.$nextTick().then(() => {
expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
});
},
);
it('should apply inactive class to all pins besides the active one', () => {
wrapper.setData({ wrapper.setData({
activeDiscussion: { activeDiscussion: {
id: notes[0].id, id: notes[0].id,
...@@ -114,6 +140,8 @@ describe('Design overlay component', () => { ...@@ -114,6 +140,8 @@ describe('Design overlay component', () => {
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findSecondBadge().classes()).toContain('inactive'); expect(findSecondBadge().classes()).toContain('inactive');
expect(findFirstBadge().classes()).not.toContain('inactive');
});
}); });
}); });
}); });
......
export default {
id: 'discussion-id-1',
resolved: false,
resolvable: true,
notes: [
{
id: 'note-id-1',
index: 1,
position: {
height: 100,
width: 100,
x: 10,
y: 15,
},
author: {
name: 'John',
webUrl: 'link-to-john-profile',
},
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
},
resolved: false,
},
{
id: 'note-id-3',
index: 3,
position: {
height: 50,
width: 50,
x: 25,
y: 25,
},
author: {
name: 'Mary',
webUrl: 'link-to-mary-profile',
},
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
},
resolved: false,
},
],
};
export default [ import DISCUSSION_1 from './discussion';
{
id: 'note-id-1', const DISCUSSION_2 = {
index: 1, id: 'discussion-id-2',
position: { notes: {
height: 100, nodes: [
width: 100,
x: 10,
y: 15,
},
author: {
name: 'John',
webUrl: 'link-to-john-profile',
},
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
},
discussion: {
id: 'discussion-id-1',
},
resolved: false,
},
{ {
id: 'note-id-2', id: 'note-id-2',
index: 2, index: 2,
...@@ -38,9 +21,24 @@ export default [ ...@@ -38,9 +21,24 @@ export default [
userPermissions: { userPermissions: {
adminNote: true, adminNote: true,
}, },
resolved: true,
},
],
},
};
export default [
{
...DISCUSSION_1.notes[0],
discussion: { discussion: {
id: 'discussion-id-2', id: DISCUSSION_1.id,
notes: {
nodes: DISCUSSION_1.notes,
}, },
resolved: true, },
},
{
...DISCUSSION_2.notes.nodes[0],
discussion: DISCUSSION_2,
}, },
]; ];
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