Commit d417f575 authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Nicolò Maria Mezzopera

Implement resolving design comments

- resolve discussion on click
- resolve discussion on comment with checkbox
- toggle resolved discussions
- toggle comments for resolved discussions
- change resolved discussions styles
parent bcf3d865
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
required: true, required: true,
}, },
label: { label: {
type: String, type: Number,
required: false, required: false,
default: null, default: null,
}, },
......
<script> <script>
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import allVersionsMixin from '../../mixins/all_versions'; import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/createNote.mutation.graphql'; import createNoteMutation from '../../graphql/mutations/createNote.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql'; import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql'; import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
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';
import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update'; import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
export default { export default {
components: { components: {
...@@ -16,6 +21,14 @@ export default { ...@@ -16,6 +21,14 @@ export default {
DesignNote, DesignNote,
ReplyPlaceholder, ReplyPlaceholder,
DesignReplyForm, DesignReplyForm,
GlIcon,
GlLoadingIcon,
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
}, },
mixins: [allVersionsMixin], mixins: [allVersionsMixin],
props: { props: {
...@@ -31,15 +44,15 @@ export default { ...@@ -31,15 +44,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
discussionIndex: {
type: Number,
required: true,
},
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
}, },
apollo: { apollo: {
activeDiscussion: { activeDiscussion: {
...@@ -49,6 +62,7 @@ export default { ...@@ -49,6 +62,7 @@ export default {
// We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
// We don't want scrollIntoView to be triggered from the discussion click itself // We don't want scrollIntoView to be triggered from the discussion click itself
if ( if (
this.resolvedDiscussionsExpanded &&
discussionId && discussionId &&
data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin && data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
discussionId === this.discussion.notes[0].id discussionId === this.discussion.notes[0].id
...@@ -66,6 +80,9 @@ export default { ...@@ -66,6 +80,9 @@ export default {
discussionComment: '', discussionComment: '',
isFormRendered: false, isFormRendered: false,
activeDiscussion: {}, activeDiscussion: {},
isResolving: false,
shouldChangeResolvedStatus: false,
areRepliesCollapsed: this.discussion.resolved,
}; };
}, },
computed: { computed: {
...@@ -87,6 +104,29 @@ export default { ...@@ -87,6 +104,29 @@ export default {
isDiscussionHighlighted() { isDiscussionHighlighted() {
return this.discussion.notes[0].id === this.activeDiscussion.id; return this.discussion.notes[0].id === this.activeDiscussion.id;
}, },
resolveCheckboxText() {
return this.discussion.resolved
? s__('DesignManagement|Unresolve thread')
: s__('DesignManagement|Resolve thread');
},
firstNote() {
return this.discussion.notes[0];
},
discussionReplies() {
return this.discussion.notes.slice(1);
},
areRepliesShown() {
return !this.discussion.resolved || !this.areRepliesCollapsed;
},
resolveIconName() {
return this.discussion.resolved ? 'check-circle-filled' : 'check-circle';
},
isRepliesWidgetVisible() {
return this.discussion.resolved && this.discussionReplies.length > 0;
},
isReplyPlaceholderVisible() {
return this.areRepliesShown || !this.discussionReplies.length;
},
}, },
methods: { methods: {
addDiscussionComment( addDiscussionComment(
...@@ -106,9 +146,12 @@ export default { ...@@ -106,9 +146,12 @@ export default {
onDone() { onDone() {
this.discussionComment = ''; this.discussionComment = '';
this.hideForm(); this.hideForm();
if (this.shouldChangeResolvedStatus) {
this.toggleResolvedStatus();
}
}, },
onError(err) { onCreateNoteError(err) {
this.$emit('error', err); this.$emit('createNoteError', err);
}, },
hideForm() { hideForm() {
this.isFormRendered = false; this.isFormRendered = false;
...@@ -123,6 +166,25 @@ export default { ...@@ -123,6 +166,25 @@ export default {
} }
this.isFormRendered = false; this.isFormRendered = false;
}, },
toggleResolvedStatus() {
this.isResolving = true;
this.$apollo
.mutate({
mutation: toggleResolveDiscussionMutation,
variables: { id: this.discussion.id, resolve: !this.discussion.resolved },
})
.then(({ data }) => {
if (data.errors?.length > 0) {
this.$emit('resolveDiscussionError', data.errors[0]);
}
})
.catch(err => {
this.$emit('resolveDiscussionError', err);
})
.finally(() => {
this.isResolving = false;
});
},
}, },
createNoteMutation, createNoteMutation,
}; };
...@@ -130,20 +192,69 @@ export default { ...@@ -130,20 +192,69 @@ export default {
<template> <template>
<div class="design-discussion-wrapper"> <div class="design-discussion-wrapper">
<div class="badge badge-pill" type="button">{{ discussionIndex }}</div>
<div <div
class="design-discussion bordered-box position-relative" class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
:class="{ resolved: discussion.resolved }"
type="button"
>
{{ discussion.index }}
</div>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content" data-qa-selector="design_discussion_content"
> >
<design-note <design-note
v-for="note in discussion.notes" :note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
@error="$emit('updateNoteError', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
v-gl-tooltip
:class="{ 'is-active': discussion.resolved }"
:title="resolveCheckboxText"
:aria-label="resolveCheckboxText"
type="button"
class="line-resolve-btn note-action-button gl-mr-3"
data-testid="resolve-button"
@click.stop="toggleResolvedStatus"
>
<gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" />
<gl-loading-icon v-else inline />
</button>
</template>
<template v-if="discussion.resolved" #resolvedStatus>
<p class="gl-text-gray-700 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
{{ __('Resolved by') }}
<gl-link
class="gl-text-gray-700 gl-text-decoration-none gl-font-sm link-inherit-color"
:href="discussion.resolvedBy.webUrl"
target="_blank"
>{{ discussion.resolvedBy.name }}</gl-link
>
<time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" />
</p>
</template>
</design-note>
<toggle-replies-widget
v-if="isRepliesWidgetVisible"
:collapsed="areRepliesCollapsed"
:replies="discussionReplies"
@toggle="areRepliesCollapsed = !areRepliesCollapsed"
/>
<design-note
v-for="note in discussionReplies"
v-show="areRepliesShown"
:key="note.id" :key="note.id"
:note="note" :note="note"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
@error="$emit('updateNoteError', $event)" @error="$emit('updateNoteError', $event)"
/> />
<div class="reply-wrapper"> <li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder <reply-placeholder
v-if="!isFormRendered" v-if="!isFormRendered"
class="qa-discussion-reply" class="qa-discussion-reply"
...@@ -159,7 +270,7 @@ export default { ...@@ -159,7 +270,7 @@ export default {
}" }"
:update="addDiscussionComment" :update="addDiscussionComment"
@done="onDone" @done="onDone"
@error="onError" @error="onCreateNoteError"
> >
<design-reply-form <design-reply-form
v-model="discussionComment" v-model="discussionComment"
...@@ -168,9 +279,16 @@ export default { ...@@ -168,9 +279,16 @@ export default {
@submitForm="mutate" @submitForm="mutate"
@cancelForm="hideForm" @cancelForm="hideForm"
@onBlur="handleReplyFormBlur" @onBlur="handleReplyFormBlur"
/> >
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
<input v-model="shouldChangeResolvedStatus" type="checkbox" />
{{ resolveCheckboxText }}
</label>
</template>
</design-reply-form>
</apollo-mutation> </apollo-mutation>
</div> </li>
</div> </ul>
</div> </div>
</template> </template>
...@@ -54,6 +54,9 @@ export default { ...@@ -54,6 +54,9 @@ export default {
body: this.noteText, body: this.noteText,
}; };
}, },
isEditButtonVisible() {
return !this.isEditing && this.note.userPermissions.adminNote;
},
}, },
mounted() { mounted() {
if (this.isNoteLinked) { if (this.isNoteLinked) {
...@@ -107,23 +110,28 @@ export default { ...@@ -107,23 +110,28 @@ export default {
</template> </template>
</span> </span>
</div> </div>
<button <div class="gl-display-flex">
v-if="!isEditing && note.userPermissions.adminNote" <slot name="resolveDiscussion"></slot>
v-gl-tooltip <button
type="button" v-if="isEditButtonVisible"
title="Edit comment" v-gl-tooltip
class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" type="button"
@click="isEditing = true" :title="__('Edit comment')"
> class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
<gl-icon name="pencil" class="link-highlight" /> @click="isEditing = true"
</button> >
<gl-icon name="pencil" class="link-highlight" />
</button>
</div>
</div> </div>
<div <template v-if="!isEditing">
v-if="!isEditing" <div
class="note-text js-note-text md" class="note-text js-note-text md"
data-qa-selector="note_content" data-qa-selector="note_content"
v-html="note.bodyHtml" v-html="note.bodyHtml"
></div> ></div>
<slot name="resolvedStatus"></slot>
</template>
<apollo-mutation <apollo-mutation
v-else v-else
#default="{ mutate, loading }" #default="{ mutate, loading }"
......
...@@ -108,7 +108,8 @@ export default { ...@@ -108,7 +108,8 @@ export default {
</textarea> </textarea>
</template> </template>
</markdown-field> </markdown-field>
<div class="note-form-actions d-flex justify-content-between"> <slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
<gl-deprecated-button <gl-deprecated-button
ref="submitButton" ref="submitButton"
:disabled="!hasValue || isSaving" :disabled="!hasValue || isSaving"
......
<script>
import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
import { __, n__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'ToggleNotesWidget',
components: {
GlIcon,
GlButton,
GlLink,
TimeAgoTooltip,
},
props: {
collapsed: {
type: Boolean,
required: true,
},
replies: {
type: Array,
required: true,
},
},
computed: {
lastReply() {
return this.replies[this.replies.length - 1];
},
iconName() {
return this.collapsed ? 'chevron-right' : 'chevron-down';
},
toggleText() {
return this.collapsed
? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}`
: __('Collapse replies');
},
},
};
</script>
<template>
<li
class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3"
:class="{ expanded: !collapsed }"
data-testid="toggle-comments-wrapper"
>
<gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" />
<gl-button
variant="link"
class="toggle-comments-button gl-ml-2 gl-mr-2"
@click.stop="$emit('toggle')"
>
{{ toggleText }}
</gl-button>
<template v-if="collapsed">
<span class="gl-text-gray-700">{{ __('Last reply by') }}</span>
<gl-link
:href="lastReply.author.webUrl"
target="_blank"
class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
>
{{ lastReply.author.name }}
</gl-link>
<time-ago-tooltip
:time="lastReply.createdAt"
tooltip-placement="bottom"
class="gl-text-gray-700"
/>
</template>
</li>
</template>
...@@ -33,6 +33,10 @@ export default { ...@@ -33,6 +33,10 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
}, },
apollo: { apollo: {
activeDiscussion: { activeDiscussion: {
...@@ -236,6 +240,9 @@ export default { ...@@ -236,6 +240,9 @@ export default {
isNoteInactive(note) { isNoteInactive(note) {
return this.activeDiscussion.id && this.activeDiscussion.id !== note.id; return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
}, },
designPinClass(note) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
},
}, },
}; };
</script> </script>
...@@ -254,20 +261,23 @@ export default { ...@@ -254,20 +261,23 @@ export default {
data-qa-selector="design_image_button" data-qa-selector="design_image_button"
@mouseup="onAddCommentMouseup" @mouseup="onAddCommentMouseup"
></button> ></button>
<design-note-pin <template v-for="note in notes">
v-for="(note, index) in notes" <design-note-pin
:key="note.id" v-if="resolvedDiscussionsExpanded || !note.resolved"
:label="`${index + 1}`" :key="note.id"
:repositioning="isMovingNote(note.id)" :label="note.index"
:position=" :repositioning="isMovingNote(note.id)"
isMovingNote(note.id) && movingNoteNewPosition :position="
? getNotePositionStyle(movingNoteNewPosition) isMovingNote(note.id) && movingNoteNewPosition
: getNotePositionStyle(note.position) ? getNotePositionStyle(movingNoteNewPosition)
" : getNotePositionStyle(note.position)
:class="{ inactive: isNoteInactive(note) }" "
@mousedown.stop="onNoteMousedown($event, note)" :class="designPinClass(note)"
@mouseup.stop="onNoteMouseup(note)" @mousedown.stop="onNoteMousedown($event, note)"
/> @mouseup.stop="onNoteMouseup(note)"
/>
</template>
<design-note-pin <design-note-pin
v-if="currentCommentForm" v-if="currentCommentForm"
:position="currentCommentPositionStyle" :position="currentCommentPositionStyle"
......
...@@ -35,6 +35,10 @@ export default { ...@@ -35,6 +35,10 @@ export default {
required: false, required: false,
default: 1, default: 1,
}, },
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -54,7 +58,10 @@ export default { ...@@ -54,7 +58,10 @@ export default {
}, },
computed: { computed: {
discussionStartingNotes() { discussionStartingNotes() {
return this.discussions.map(discussion => discussion.notes[0]); return this.discussions.map(discussion => ({
...discussion.notes[0],
index: discussion.index,
}));
}, },
currentCommentForm() { currentCommentForm() {
return (this.isAnnotating && this.currentAnnotationPosition) || null; return (this.isAnnotating && this.currentAnnotationPosition) || null;
...@@ -305,6 +312,7 @@ export default { ...@@ -305,6 +312,7 @@ export default {
:notes="discussionStartingNotes" :notes="discussionStartingNotes"
:current-comment-form="currentCommentForm" :current-comment-form="currentCommentForm"
:disable-commenting="isDraggingDesign" :disable-commenting="isDraggingDesign"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@openCommentForm="openCommentForm" @openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm" @closeCommentForm="closeCommentForm"
@moveNote="moveNote" @moveNote="moveNote"
......
<script>
import { s__ } from '~/locale';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
export default {
components: {
DesignDiscussion,
Participants,
GlCollapse,
GlButton,
GlPopover,
},
props: {
design: {
type: Object,
required: true,
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
},
data() {
return {
isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)),
};
},
computed: {
discussions() {
return extractDiscussions(this.design.discussions);
},
issue() {
return {
...this.design.issue,
webPath: this.design.issue.webPath.substr(1),
};
},
discussionParticipants() {
return extractParticipants(this.issue.participants);
},
resolvedDiscussions() {
return this.discussions.filter(discussion => discussion.resolved);
},
unresolvedDiscussions() {
return this.discussions.filter(discussion => !discussion.resolved);
},
resolvedCommentsToggleIcon() {
return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
},
},
methods: {
handleSidebarClick() {
this.isResolvedCommentsPopoverHidden = true;
Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 });
this.updateActiveDiscussion();
},
updateActiveDiscussion(id) {
this.$apollo.mutate({
mutation: updateActiveDiscussionMutation,
variables: {
id,
source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
},
});
},
closeCommentForm() {
this.comment = '';
this.$emit('closeCommentForm');
},
},
resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'),
cookieKey: 'hide_design_resolved_comments_popover',
};
</script>
<template>
<div class="image-notes" @click="handleSidebarClick">
<h2 class="gl-font-weight-bold gl-mt-0">
{{ issue.title }}
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
:href="issue.webUrl"
>{{ issue.webPath }}</a
>
<participants
:participants="discussionParticipants"
:show-participant-label="false"
class="gl-mb-4"
/>
<h2
v-if="unresolvedDiscussions.length === 0"
class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
data-testid="new-discussion-disclaimer"
>
{{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
</h2>
<design-discussion
v-for="discussion in unresolvedDiscussions"
:key="discussion.id"
:discussion="discussion"
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
data-testid="unresolved-discussion"
@createNoteError="$emit('onDesignDiscussionError', $event)"
@updateNoteError="$emit('updateNoteError', $event)"
@resolveDiscussionError="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
id="resolved-comments"
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
@click="$emit('toggleResolvedComments')"
>{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
</gl-button>
<gl-popover
v-if="!isResolvedCommentsPopoverHidden"
:show="!isResolvedCommentsPopoverHidden"
target="resolved-comments"
container="popovercontainer"
placement="top"
:title="s__('DesignManagement|Resolved Comments')"
>
<p>
{{
s__(
'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below',
)
}}
</p>
<a href="#" rel="noopener noreferrer" target="_blank">{{
s__('DesignManagement|Learn more about resolving comments')
}}</a>
</gl-popover>
<gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
<design-discussion
v-for="discussion in resolvedDiscussions"
:key="discussion.id"
:discussion="discussion"
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
data-testid="resolved-discussion"
@error="$emit('onDesignDiscussionError', $event)"
@updateNoteError="$emit('updateNoteError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
</gl-collapse>
</template>
<slot name="replyForm"></slot>
</div>
</template>
#import "./designNote.fragment.graphql" #import "./designNote.fragment.graphql"
#import "./designList.fragment.graphql" #import "./designList.fragment.graphql"
#import "./diffRefs.fragment.graphql" #import "./diffRefs.fragment.graphql"
#import "./discussion_resolved_status.fragment.graphql"
fragment DesignItem on Design { fragment DesignItem on Design {
...DesignListItem ...DesignListItem
...@@ -12,6 +13,7 @@ fragment DesignItem on Design { ...@@ -12,6 +13,7 @@ fragment DesignItem on Design {
nodes { nodes {
id id
replyId replyId
...ResolvedStatus
notes { notes {
nodes { nodes {
...DesignNote ...DesignNote
......
...@@ -10,6 +10,7 @@ fragment DesignNote on Note { ...@@ -10,6 +10,7 @@ fragment DesignNote on Note {
body body
bodyHtml bodyHtml
createdAt createdAt
resolved
position { position {
diffRefs { diffRefs {
...DesignDiffRefs ...DesignDiffRefs
......
fragment ResolvedStatus on Discussion {
resolvable
resolved
resolvedAt
resolvedBy {
name
webUrl
}
}
#import "../fragments/designNote.fragment.graphql"
#import "../fragments/discussion_resolved_status.fragment.graphql"
mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion {
id
...ResolvedStatus
notes {
nodes {
...DesignNote
}
}
}
errors
}
}
<script> <script>
import { ApolloMutation } from 'vue-apollo';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import allVersionsMixin from '../../mixins/all_versions'; import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue'; import Toolbar from '../../components/toolbar/index.vue';
import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.vue'; import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignScaler from '../../components/design_scaler.vue'; import DesignScaler from '../../components/design_scaler.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import DesignPresentation from '../../components/design_presentation.vue'; import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql'; import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import appDataQuery from '../../graphql/queries/appData.query.graphql'; import appDataQuery from '../../graphql/queries/appData.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql'; import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
...@@ -20,7 +19,6 @@ import updateActiveDiscussionMutation from '../../graphql/mutations/update_activ ...@@ -20,7 +19,6 @@ import updateActiveDiscussionMutation from '../../graphql/mutations/update_activ
import { import {
extractDiscussions, extractDiscussions,
extractDesign, extractDesign,
extractParticipants,
updateImageDiffNoteOptimisticResponse, updateImageDiffNoteOptimisticResponse,
} from '../../utils/design_management_utils'; } from '../../utils/design_management_utils';
import { import {
...@@ -43,15 +41,14 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; ...@@ -43,15 +41,14 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
export default { export default {
components: { components: {
ApolloMutation, ApolloMutation,
DesignReplyForm,
DesignPresentation, DesignPresentation,
DesignDiscussion,
DesignScaler, DesignScaler,
DesignDestroyer, DesignDestroyer,
Toolbar, Toolbar,
DesignReplyForm,
GlLoadingIcon, GlLoadingIcon,
GlAlert, GlAlert,
Participants, DesignSidebar,
}, },
mixins: [allVersionsMixin], mixins: [allVersionsMixin],
props: { props: {
...@@ -69,6 +66,7 @@ export default { ...@@ -69,6 +66,7 @@ export default {
errorMessage: '', errorMessage: '',
issueIid: '', issueIid: '',
scale: 1, scale: 1,
resolvedDiscussionsExpanded: false,
}; };
}, },
apollo: { apollo: {
...@@ -103,20 +101,17 @@ export default { ...@@ -103,20 +101,17 @@ export default {
return this.$apollo.queries.design.loading && !this.design.filename; return this.$apollo.queries.design.loading && !this.design.filename;
}, },
discussions() { discussions() {
if (!this.design.discussions) {
return [];
}
return extractDiscussions(this.design.discussions); return extractDiscussions(this.design.discussions);
}, },
discussionParticipants() {
return extractParticipants(this.design.issue.participants);
},
markdownPreviewPath() { markdownPreviewPath() {
return `/${this.projectPath}/preview_markdown?target_type=Issue`; return `/${this.projectPath}/preview_markdown?target_type=Issue`;
}, },
isSubmitButtonDisabled() { isSubmitButtonDisabled() {
return this.comment.trim().length === 0; return this.comment.trim().length === 0;
}, },
renderDiscussions() {
return this.discussions.length || this.annotationCoordinates;
},
designVariables() { designVariables() {
return { return {
fullPath: this.projectPath, fullPath: this.projectPath,
...@@ -144,15 +139,19 @@ export default { ...@@ -144,15 +139,19 @@ export default {
}, },
}; };
}, },
issue() {
return {
...this.design.issue,
webPath: this.design.issue.webPath.substr(1),
};
},
isAnnotating() { isAnnotating() {
return Boolean(this.annotationCoordinates); return Boolean(this.annotationCoordinates);
}, },
resolvedDiscussions() {
return this.discussions.filter(discussion => discussion.resolved);
},
},
watch: {
resolvedDiscussions(val) {
if (!val.length) {
this.resolvedDiscussionsExpanded = false;
}
},
}, },
mounted() { mounted() {
Mousetrap.bind('esc', this.closeDesign); Mousetrap.bind('esc', this.closeDesign);
...@@ -250,6 +249,9 @@ export default { ...@@ -250,6 +249,9 @@ export default {
onDesignDeleteError(e) { onDesignDeleteError(e) {
this.onError(designDeletionError({ singular: true }), e); this.onError(designDeletionError({ singular: true }), e);
}, },
onResolveDiscussionError(e) {
this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
},
openCommentForm(annotationCoordinates) { openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates; this.annotationCoordinates = annotationCoordinates;
}, },
...@@ -281,6 +283,9 @@ export default { ...@@ -281,6 +283,9 @@ export default {
}, },
}); });
}, },
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
}, },
createImageDiffNoteMutation, createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME, DESIGNS_ROUTE_NAME,
...@@ -323,6 +328,7 @@ export default { ...@@ -323,6 +328,7 @@ export default {
:discussions="discussions" :discussions="discussions"
:is-annotating="isAnnotating" :is-annotating="isAnnotating"
:scale="scale" :scale="scale"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@openCommentForm="openCommentForm" @openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm" @closeCommentForm="closeCommentForm"
@moveNote="onMoveNote" @moveNote="onMoveNote"
...@@ -332,33 +338,19 @@ export default { ...@@ -332,33 +338,19 @@ export default {
<design-scaler @scale="scale = $event" /> <design-scaler @scale="scale = $event" />
</div> </div>
</div> </div>
<div class="image-notes" @click="updateActiveDiscussion()"> <design-sidebar
<h2 class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0"> :design="design"
{{ issue.title }} :resolved-discussions-expanded="resolvedDiscussionsExpanded"
</h2> :markdown-preview-path="markdownPreviewPath"
<a class="text-tertiary text-decoration-none mb-3 d-block" :href="issue.webUrl">{{ @onDesignDiscussionError="onDesignDiscussionError"
issue.webPath @onCreateImageDiffNoteError="onCreateImageDiffNoteError"
}}</a> @updateNoteError="onUpdateNoteError"
<participants @resolveDiscussionError="onResolveDiscussionError"
:participants="discussionParticipants" @toggleResolvedComments="toggleResolvedComments"
:show-participant-label="false" >
class="mb-4" <template #replyForm>
/>
<template v-if="renderDiscussions">
<design-discussion
v-for="(discussion, index) in discussions"
:key="discussion.id"
:discussion="discussion"
:design-id="id"
:noteable-id="design.id"
:discussion-index="index + 1"
:markdown-preview-path="markdownPreviewPath"
@error="onDesignDiscussionError"
@updateNoteError="onUpdateNoteError"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
<apollo-mutation <apollo-mutation
v-if="annotationCoordinates" v-if="isAnnotating"
#default="{ mutate, loading }" #default="{ mutate, loading }"
:mutation="$options.createImageDiffNoteMutation" :mutation="$options.createImageDiffNoteMutation"
:variables="{ :variables="{
...@@ -374,13 +366,9 @@ export default { ...@@ -374,13 +366,9 @@ export default {
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
@submitForm="mutate" @submitForm="mutate"
@cancelForm="closeCommentForm" @cancelForm="closeCommentForm"
/> /> </apollo-mutation
</apollo-mutation> ></template>
</template> </design-sidebar>
<h2 v-else class="new-discussion-disclaimer gl-font-base m-0">
{{ __("Click the image where you'd like to start a new discussion") }}
</h2>
</div>
</template> </template>
</div> </div>
</template> </template>
...@@ -95,6 +95,10 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) = ...@@ -95,6 +95,10 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
__typename: 'Discussion', __typename: 'Discussion',
id: createImageDiffNote.note.discussion.id, id: createImageDiffNote.note.discussion.id,
replyId: createImageDiffNote.note.discussion.replyId, replyId: createImageDiffNote.note.discussion.replyId,
resolvable: true,
resolved: false,
resolvedAt: null,
resolvedBy: null,
notes: { notes: {
__typename: 'NoteConnection', __typename: 'NoteConnection',
nodes: [createImageDiffNote.note], nodes: [createImageDiffNote.note],
......
...@@ -21,8 +21,9 @@ export const extractNodes = elements => elements.edges.map(({ node }) => node); ...@@ -21,8 +21,9 @@ export const extractNodes = elements => elements.edges.map(({ node }) => node);
*/ */
export const extractDiscussions = discussions => export const extractDiscussions = discussions =>
discussions.nodes.map(discussion => ({ discussions.nodes.map((discussion, index) => ({
...discussion, ...discussion,
index: index + 1,
notes: discussion.notes.nodes, notes: discussion.notes.nodes,
})); }));
......
...@@ -20,6 +20,20 @@ ...@@ -20,6 +20,20 @@
} }
} }
} }
.badge.badge-pill {
display: flex;
height: 28px;
width: 28px;
background-color: $blue-400;
color: $white;
border: $white 1px solid;
border-radius: 50%;
&.resolved {
background-color: $gray-700;
}
}
} }
.design-presentation-wrapper { .design-presentation-wrapper {
...@@ -52,14 +66,31 @@ ...@@ -52,14 +66,31 @@
min-width: 400px; min-width: 400px;
flex-basis: 28%; flex-basis: 28%;
.link-inherit-color {
&:hover,
&:active,
&:focus {
color: inherit;
text-decoration: none;
}
}
.toggle-comments {
line-height: 20px;
border-top: 1px solid $border-color;
&.expanded {
border-bottom: 1px solid $border-color;
}
.toggle-comments-button:focus {
text-decoration: none;
color: $blue-600;
}
}
.badge.badge-pill { .badge.badge-pill {
margin-left: $gl-padding; margin-left: $gl-padding;
background-color: $blue-400;
color: $white;
border: $white 1px solid;
min-height: 28px;
padding: 7px 10px;
border-radius: $gl-padding;
} }
.design-discussion { .design-discussion {
......
---
title: "[Frontend] Resolvable design discussions"
merge_request: 32399
author:
type: added
...@@ -177,6 +177,22 @@ Different discussions have different pin numbers: ...@@ -177,6 +177,22 @@ Different discussions have different pin numbers:
From GitLab 12.5 on, new discussions will be outputted to the issue activity, From GitLab 12.5 on, new discussions will be outputted to the issue activity,
so that everyone involved can participate in the discussion. so that everyone involved can participate in the discussion.
## Resolve Design threads
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13049) in GitLab 13.1.
Discussion threads can be resolved on Designs. You can mark a thread as resolved
or unresolved by clicking the **Resolve thread** icon at the first comment of the
discussion.
![Resolve thread icon](img/resolve_design-discussion_icon_v13_1.png)
Pinned comments can also be resolved or unresolved in their threads.
When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve
the thread once published.
![Resolve checkbox](img/resolve_design-discussion_checkbox_v13_1.png)
## Referring to designs in Markdown ## Referring to designs in Markdown
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217160) in **GitLab 13.1**. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217160) in **GitLab 13.1**.
......
...@@ -4451,9 +4451,6 @@ msgstr "" ...@@ -4451,9 +4451,6 @@ msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page" msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr "" msgstr ""
msgid "Click the image where you'd like to start a new discussion"
msgstr ""
msgid "Click to expand it." msgid "Click to expand it."
msgstr "" msgstr ""
...@@ -5528,6 +5525,9 @@ msgstr "" ...@@ -5528,6 +5525,9 @@ msgstr ""
msgid "Collapse child epics" msgid "Collapse child epics"
msgstr "" msgstr ""
msgid "Collapse replies"
msgstr ""
msgid "Collapse sidebar" msgid "Collapse sidebar"
msgstr "" msgstr ""
...@@ -7519,9 +7519,15 @@ msgstr "" ...@@ -7519,9 +7519,15 @@ msgstr ""
msgid "DesignManagement|Cancel comment update confirmation" msgid "DesignManagement|Cancel comment update confirmation"
msgstr "" msgstr ""
msgid "DesignManagement|Click the image where you'd like to start a new discussion"
msgstr ""
msgid "DesignManagement|Comment" msgid "DesignManagement|Comment"
msgstr "" msgstr ""
msgid "DesignManagement|Comments you resolve can be viewed and unresolved by going to the \"Resolved Comments\" section below"
msgstr ""
msgid "DesignManagement|Could not add a new comment. Please try again." msgid "DesignManagement|Could not add a new comment. Please try again."
msgstr "" msgstr ""
...@@ -7567,9 +7573,18 @@ msgstr "" ...@@ -7567,9 +7573,18 @@ msgstr ""
msgid "DesignManagement|Keep comment" msgid "DesignManagement|Keep comment"
msgstr "" msgstr ""
msgid "DesignManagement|Learn more about resolving comments"
msgstr ""
msgid "DesignManagement|Requested design version does not exist. Showing latest version instead" msgid "DesignManagement|Requested design version does not exist. Showing latest version instead"
msgstr "" msgstr ""
msgid "DesignManagement|Resolve thread"
msgstr ""
msgid "DesignManagement|Resolved Comments"
msgstr ""
msgid "DesignManagement|Save comment" msgid "DesignManagement|Save comment"
msgstr "" msgstr ""
...@@ -7582,6 +7597,9 @@ msgstr "" ...@@ -7582,6 +7597,9 @@ msgstr ""
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance." msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
msgstr "" msgstr ""
msgid "DesignManagement|Unresolve thread"
msgstr ""
msgid "DesignManagement|Upload designs" msgid "DesignManagement|Upload designs"
msgstr "" msgstr ""
...@@ -18657,6 +18675,9 @@ msgstr "" ...@@ -18657,6 +18675,9 @@ msgstr ""
msgid "Resolved all discussions." msgid "Resolved all discussions."
msgstr "" msgstr ""
msgid "Resolved by"
msgstr ""
msgid "Resolved by %{name}" msgid "Resolved by %{name}"
msgstr "" msgstr ""
......
...@@ -26,7 +26,7 @@ describe('Design note pin component', () => { ...@@ -26,7 +26,7 @@ describe('Design note pin component', () => {
}); });
it('should match the snapshot of note with index', () => { it('should match the snapshot of note with index', () => {
createComponent({ label: '1' }); createComponent({ label: 1 });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
......
...@@ -50,12 +50,18 @@ exports[`Design note component should match the snapshot 1`] = ` ...@@ -50,12 +50,18 @@ exports[`Design note component should match the snapshot 1`] = `
</span> </span>
</div> </div>
<!----> <div
class="gl-display-flex"
>
<!---->
</div>
</div> </div>
<div <div
class="note-text js-note-text md" class="note-text js-note-text md"
data-qa-selector="note_content" data-qa-selector="note_content"
/> />
</timeline-entry-item-stub> </timeline-entry-item-stub>
`; `;
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo'; import { GlLoadingIcon } from '@gitlab/ui';
import notes from '../../mock_data/notes';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue'; import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import createNoteMutation from '~/design_management/graphql/mutations/createNote.mutation.graphql'; import createNoteMutation from '~/design_management/graphql/mutations/createNote.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';
const discussion = {
id: '0',
resolved: false,
resolvable: true,
notes,
};
describe('Design discussions component', () => { describe('Design discussions component', () => {
let wrapper; let wrapper;
const findDesignNotes = () => wrapper.findAll(DesignNote);
const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder); const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder);
const findReplyForm = () => wrapper.find(DesignReplyForm); const findReplyForm = () => wrapper.find(DesignReplyForm);
const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget);
const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]');
const findResolveIcon = () => wrapper.find('[data-testid="resolve-icon"]');
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
const mutationVariables = { const mutationVariables = {
mutation: createNoteMutation, mutation: createNoteMutation,
...@@ -29,19 +46,10 @@ describe('Design discussions component', () => { ...@@ -29,19 +46,10 @@ describe('Design discussions component', () => {
}; };
function createComponent(props = {}, data = {}) { function createComponent(props = {}, data = {}) {
wrapper = shallowMount(DesignDiscussion, { wrapper = mount(DesignDiscussion, {
propsData: { propsData: {
discussion: { resolvedDiscussionsExpanded: true,
id: '0', discussion,
notes: [
{
id: '1',
},
{
id: '2',
},
],
},
noteableId: 'noteable-id', noteableId: 'noteable-id',
designId: 'design-id', designId: 'design-id',
discussionIndex: 1, discussionIndex: 1,
...@@ -52,11 +60,12 @@ describe('Design discussions component', () => { ...@@ -52,11 +60,12 @@ describe('Design discussions component', () => {
...data, ...data,
}; };
}, },
stubs: { mocks: {
ReplyPlaceholder, $apollo,
ApolloMutation, $route: {
hash: '#note_1',
},
}, },
mocks: { $apollo },
}); });
} }
...@@ -64,14 +73,139 @@ describe('Design discussions component', () => { ...@@ -64,14 +73,139 @@ describe('Design discussions component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders correct amount of discussion notes', () => { describe('when discussion is not resolvable', () => {
createComponent(); beforeEach(() => {
expect(wrapper.findAll(DesignNote)).toHaveLength(2); createComponent({
discussion: {
...discussion,
resolvable: false,
},
});
});
it('does not render an icon to resolve a thread', () => {
expect(findResolveIcon().exists()).toBe(false);
});
it('does not render a checkbox in reply form', () => {
findReplyPlaceholder().vm.$emit('onMouseDown');
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().exists()).toBe(false);
});
});
}); });
it('renders reply placeholder by default', () => { describe('when discussion is unresolved', () => {
createComponent(); beforeEach(() => {
expect(findReplyPlaceholder().exists()).toBe(true); createComponent();
});
it('renders correct amount of discussion notes', () => {
expect(findDesignNotes()).toHaveLength(2);
expect(findDesignNotes().wrappers.every(w => w.isVisible())).toBe(true);
});
it('renders reply placeholder', () => {
expect(findReplyPlaceholder().isVisible()).toBe(true);
});
it('does not render toggle replies widget', () => {
expect(findRepliesWidget().exists()).toBe(false);
});
it('renders a correct icon to resolve a thread', () => {
expect(findResolveIcon().props('name')).toBe('check-circle');
});
it('renders a checkbox with Resolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onMouseDown');
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Resolve thread');
});
});
it('does not render resolved message', () => {
expect(findResolvedMessage().exists()).toBe(false);
});
});
describe('when discussion is resolved', () => {
beforeEach(() => {
createComponent({
discussion: {
...discussion,
resolved: true,
resolvedBy: notes[0].author,
resolvedAt: '2020-05-08T07:10:45Z',
},
});
});
it('shows only the first note', () => {
expect(
findDesignNotes()
.at(0)
.isVisible(),
).toBe(true);
expect(
findDesignNotes()
.at(1)
.isVisible(),
).toBe(false);
});
it('renders resolved message', () => {
expect(findResolvedMessage().exists()).toBe(true);
});
it('does not show renders reply placeholder', () => {
expect(findReplyPlaceholder().isVisible()).toBe(false);
});
it('renders toggle replies widget with correct props', () => {
expect(findRepliesWidget().exists()).toBe(true);
expect(findRepliesWidget().props()).toEqual({
collapsed: true,
replies: notes.slice(1),
});
});
it('renders a correct icon to resolve a thread', () => {
expect(findResolveIcon().props('name')).toBe('check-circle-filled');
});
describe('when replies are expanded', () => {
beforeEach(() => {
findRepliesWidget().vm.$emit('toggle');
return wrapper.vm.$nextTick();
});
it('renders replies widget with collapsed prop equal to false', () => {
expect(findRepliesWidget().props('collapsed')).toBe(false);
});
it('renders the second note', () => {
expect(
findDesignNotes()
.at(1)
.isVisible(),
).toBe(true);
});
it('renders a reply placeholder', () => {
expect(findReplyPlaceholder().isVisible()).toBe(true);
});
it('renders a checkbox with Unresolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onMouseDown');
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Unresolve thread');
});
});
});
}); });
it('hides reply placeholder and opens form on placeholder click', () => { it('hides reply placeholder and opens form on placeholder click', () => {
...@@ -120,7 +254,7 @@ describe('Design discussions component', () => { ...@@ -120,7 +254,7 @@ describe('Design discussions component', () => {
{}, {},
{ {
activeDiscussion: { activeDiscussion: {
id: '1', id: notes[0].id,
source: 'pin', source: 'pin',
}, },
}, },
...@@ -148,4 +282,35 @@ describe('Design discussions component', () => { ...@@ -148,4 +282,35 @@ describe('Design discussions component', () => {
expect(findReplyForm().exists()).toBe(true); expect(findReplyForm().exists()).toBe(true);
}); });
}); });
it('calls toggleResolveDiscussion mutation on resolve thread button click', () => {
createComponent();
findResolveButton().trigger('click');
expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation,
variables: {
id: discussion.id,
resolve: true,
},
});
return wrapper.vm.$nextTick(() => {
expect(findResolveLoadingIcon().exists()).toBe(true);
});
});
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
createComponent({}, { discussionComment: 'test', isFormRendered: true });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
return mutate().then(() => {
expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation,
variables: {
id: discussion.id,
resolve: true,
},
});
});
});
}); });
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import notes from '../../mock_data/notes';
describe('Toggle replies widget component', () => {
let wrapper;
const findToggleWrapper = () => wrapper.find('[data-testid="toggle-comments-wrapper"]');
const findIcon = () => wrapper.find(GlIcon);
const findButton = () => wrapper.find(GlButton);
const findAuthorLink = () => wrapper.find(GlLink);
const findTimeAgo = () => wrapper.find(TimeAgoTooltip);
function createComponent(props = {}) {
wrapper = shallowMount(ToggleRepliesWidget, {
propsData: {
collapsed: true,
replies: notes,
...props,
},
});
}
afterEach(() => {
wrapper.destroy();
});
describe('when replies are collapsed', () => {
beforeEach(() => {
createComponent();
});
it('should not have expanded class', () => {
expect(findToggleWrapper().classes()).not.toContain('expanded');
});
it('should render chevron-right icon', () => {
expect(findIcon().props('name')).toBe('chevron-right');
});
it('should have replies length on button', () => {
expect(findButton().text()).toBe('2 replies');
});
it('should render a link to the last reply author', () => {
expect(findAuthorLink().exists()).toBe(true);
expect(findAuthorLink().text()).toBe(notes[1].author.name);
expect(findAuthorLink().attributes('href')).toBe(notes[1].author.webUrl);
});
it('should render correct time ago tooltip', () => {
expect(findTimeAgo().exists()).toBe(true);
expect(findTimeAgo().props('time')).toBe(notes[1].createdAt);
});
});
describe('when replies are expanded', () => {
beforeEach(() => {
createComponent({ collapsed: false });
});
it('should have expanded class', () => {
expect(findToggleWrapper().classes()).toContain('expanded');
});
it('should render chevron-down icon', () => {
expect(findIcon().props('name')).toBe('chevron-down');
});
it('should have Collapse replies text on button', () => {
expect(findButton().text()).toBe('Collapse replies');
});
it('should not have a link to the last reply author', () => {
expect(findAuthorLink().exists()).toBe(false);
});
it('should not render time ago tooltip', () => {
expect(findTimeAgo().exists()).toBe(false);
});
});
it('should emit toggle event on icon click', () => {
createComponent();
findIcon().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.emitted('toggle')).toHaveLength(1);
});
it('should emit toggle event on button click', () => {
createComponent();
findButton().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.emitted('toggle')).toHaveLength(1);
});
});
...@@ -12,6 +12,7 @@ describe('Design overlay component', () => { ...@@ -12,6 +12,7 @@ describe('Design overlay component', () => {
const mockDimensions = { width: 100, height: 100 }; const mockDimensions = { width: 100, height: 100 };
const mockNoteNotAuthorised = { const mockNoteNotAuthorised = {
id: 'note-not-authorised', id: 'note-not-authorised',
index: 1,
discussion: { id: 'discussion-not-authorised' }, discussion: { id: 'discussion-not-authorised' },
position: { position: {
x: 1, x: 1,
...@@ -19,6 +20,7 @@ describe('Design overlay component', () => { ...@@ -19,6 +20,7 @@ describe('Design overlay component', () => {
...mockDimensions, ...mockDimensions,
}, },
userPermissions: {}, userPermissions: {},
resolved: false,
}; };
const findOverlay = () => wrapper.find('.image-diff-overlay'); const findOverlay = () => wrapper.find('.image-diff-overlay');
...@@ -43,6 +45,7 @@ describe('Design overlay component', () => { ...@@ -43,6 +45,7 @@ describe('Design overlay component', () => {
top: '0', top: '0',
left: '0', left: '0',
}, },
resolvedDiscussionsExpanded: false,
...props, ...props,
}, },
data() { data() {
...@@ -88,19 +91,46 @@ describe('Design overlay component', () => { ...@@ -88,19 +91,46 @@ describe('Design overlay component', () => {
}); });
describe('with notes', () => { describe('with notes', () => {
beforeEach(() => { it('should render only the first note', () => {
createComponent({ createComponent({
notes, notes,
}); });
expect(findAllNotes()).toHaveLength(1);
}); });
it('should render a correct amount of notes', () => { describe('with resolved discussions toggle expanded', () => {
expect(findAllNotes()).toHaveLength(notes.length); beforeEach(() => {
}); createComponent({
notes,
resolvedDiscussionsExpanded: true,
});
});
it('should render all notes', () => {
expect(findAllNotes()).toHaveLength(notes.length);
});
it('should have set the correct position for each note badge', () => {
expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;');
expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;');
});
it('should apply resolved class to the resolved note pin', () => {
expect(findSecondBadge().classes()).toContain('resolved');
});
it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
wrapper.setData({
activeDiscussion: {
id: notes[0].id,
source: 'discussion',
},
});
it('should have a correct style for each note badge', () => { return wrapper.vm.$nextTick().then(() => {
expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;'); expect(findSecondBadge().classes()).toContain('inactive');
expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;'); });
});
}); });
it('should recalculate badges positions on window resize', () => { it('should recalculate badges positions on window resize', () => {
...@@ -144,19 +174,6 @@ describe('Design overlay component', () => { ...@@ -144,19 +174,6 @@ describe('Design overlay component', () => {
expect(mutate).toHaveBeenCalledWith(mutationVariables); expect(mutate).toHaveBeenCalledWith(mutationVariables);
}); });
}); });
it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
wrapper.setData({
activeDiscussion: {
id: notes[0].id,
source: 'discussion',
},
});
return wrapper.vm.$nextTick().then(() => {
expect(findSecondBadge().classes()).toContain('inactive');
});
});
}); });
describe('when moving notes', () => { describe('when moving notes', () => {
......
...@@ -17,7 +17,13 @@ describe('Design management design presentation component', () => { ...@@ -17,7 +17,13 @@ describe('Design management design presentation component', () => {
let wrapper; let wrapper;
function createComponent( function createComponent(
{ image, imageName, discussions = [], isAnnotating = false } = {}, {
image,
imageName,
discussions = [],
isAnnotating = false,
resolvedDiscussionsExpanded = false,
} = {},
data = {}, data = {},
stubs = {}, stubs = {},
) { ) {
...@@ -27,6 +33,7 @@ describe('Design management design presentation component', () => { ...@@ -27,6 +33,7 @@ describe('Design management design presentation component', () => {
imageName, imageName,
discussions, discussions,
isAnnotating, isAnnotating,
resolvedDiscussionsExpanded,
}, },
stubs, stubs,
}); });
......
import { shallowMount } from '@vue/test-utils';
import { GlCollapse, GlPopover } from '@gitlab/ui';
import Cookies from 'js-cookie';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import design from '../mock_data/design';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
const updateActiveDiscussionMutationVariables = {
mutation: updateActiveDiscussionMutation,
variables: {
id: design.discussions.nodes[0].notes.nodes[0].id,
source: 'discussion',
},
};
const $route = {
params: {
id: '1',
},
};
const cookieKey = 'hide_design_resolved_comments_popover';
const mutate = jest.fn().mockResolvedValue();
describe('Design management design sidebar component', () => {
let wrapper;
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
const findFirstDiscussion = () => findDiscussions().at(0);
const findUnresolvedDiscussions = () => wrapper.findAll('[data-testid="unresolved-discussion"]');
const findResolvedDiscussions = () => wrapper.findAll('[data-testid="resolved-discussion"]');
const findParticipants = () => wrapper.find(Participants);
const findCollapsible = () => wrapper.find(GlCollapse);
const findToggleResolvedCommentsButton = () => wrapper.find('[data-testid="resolved-comments"]');
const findPopover = () => wrapper.find(GlPopover);
const findNewDiscussionDisclaimer = () =>
wrapper.find('[data-testid="new-discussion-disclaimer"]');
function createComponent(props = {}) {
wrapper = shallowMount(DesignSidebar, {
propsData: {
design,
resolvedDiscussionsExpanded: false,
markdownPreviewPath: '',
...props,
},
mocks: {
$route,
$apollo: {
mutate,
},
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders participants', () => {
createComponent();
expect(findParticipants().exists()).toBe(true);
});
it('passes the correct amount of participants to the Participants component', () => {
createComponent();
expect(findParticipants().props('participants')).toHaveLength(1);
});
describe('when has no discussions', () => {
beforeEach(() => {
createComponent({
design: {
...design,
discussions: {
nodes: [],
},
},
});
});
it('does not render discussions', () => {
expect(findDiscussions().exists()).toBe(false);
});
it('renders a message about possibility to create a new discussion', () => {
expect(findNewDiscussionDisclaimer().exists()).toBe(true);
});
});
describe('when has discussions', () => {
beforeEach(() => {
Cookies.set(cookieKey, true);
createComponent();
});
it('renders correct amount of unresolved discussions', () => {
expect(findUnresolvedDiscussions()).toHaveLength(1);
});
it('renders correct amount of resolved discussions', () => {
expect(findResolvedDiscussions()).toHaveLength(1);
});
it('has resolved comments collapsible collapsed', () => {
expect(findCollapsible().attributes('visible')).toBeUndefined();
});
it('emits toggleResolveComments event on resolve comments button click', () => {
findToggleResolvedCommentsButton().vm.$emit('click');
expect(wrapper.emitted('toggleResolvedComments')).toHaveLength(1);
});
it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', () => {
expect(findCollapsible().attributes('visible')).toBeUndefined();
wrapper.setProps({
resolvedDiscussionsExpanded: true,
});
return wrapper.vm.$nextTick().then(() => {
expect(findCollapsible().attributes('visible')).toBe('true');
});
});
it('does not popover about resolved comments', () => {
expect(findPopover().exists()).toBe(false);
});
it('sends a mutation to set an active discussion when clicking on a discussion', () => {
findFirstDiscussion().trigger('click');
expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
});
it('sends a mutation to reset an active discussion when clicking outside of discussion', () => {
wrapper.trigger('click');
expect(mutate).toHaveBeenCalledWith({
...updateActiveDiscussionMutationVariables,
variables: { id: undefined, source: 'discussion' },
});
});
it('emits correct event on discussion create note error', () => {
findFirstDiscussion().vm.$emit('createNoteError', 'payload');
expect(wrapper.emitted('onDesignDiscussionError')).toEqual([['payload']]);
});
it('emits correct event on discussion update note error', () => {
findFirstDiscussion().vm.$emit('updateNoteError', 'payload');
expect(wrapper.emitted('updateNoteError')).toEqual([['payload']]);
});
it('emits correct event on discussion resolve error', () => {
findFirstDiscussion().vm.$emit('resolveDiscussionError', 'payload');
expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]);
});
});
describe('when all discussions are resolved', () => {
beforeEach(() => {
createComponent({
design: {
...design,
discussions: {
nodes: [
{
id: 'discussion-id',
replyId: 'discussion-reply-id',
resolved: true,
notes: {
nodes: [
{
id: 'note-id',
body: '123',
author: {
name: 'Administrator',
username: 'root',
webUrl: 'link-to-author',
avatarUrl: 'link-to-avatar',
},
},
],
},
},
],
},
},
});
});
it('renders a message about possibility to create a new discussion', () => {
expect(findNewDiscussionDisclaimer().exists()).toBe(true);
});
it('does not render unresolved discussions', () => {
expect(findUnresolvedDiscussions()).toHaveLength(0);
});
});
describe('when showing resolved discussions for the first time', () => {
beforeEach(() => {
Cookies.set(cookieKey, false);
createComponent();
});
it('renders a popover if we show resolved comments collapsible for the first time', () => {
expect(findPopover().exists()).toBe(true);
});
it('dismisses a popover on the outside click', () => {
wrapper.trigger('click');
return wrapper.vm.$nextTick(() => {
expect(findPopover().exists()).toBe(false);
});
});
it(`sets a ${cookieKey} cookie on clicking outside the popover`, () => {
jest.spyOn(Cookies, 'set');
wrapper.trigger('click');
expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
});
});
});
...@@ -29,6 +29,7 @@ export default { ...@@ -29,6 +29,7 @@ export default {
{ {
id: 'discussion-id', id: 'discussion-id',
replyId: 'discussion-reply-id', replyId: 'discussion-reply-id',
resolved: false,
notes: { notes: {
nodes: [ nodes: [
{ {
...@@ -44,6 +45,25 @@ export default { ...@@ -44,6 +45,25 @@ export default {
], ],
}, },
}, },
{
id: 'discussion-resolved',
replyId: 'discussion-reply-resolved',
resolved: true,
notes: {
nodes: [
{
id: 'note-resolved',
body: '123',
author: {
name: 'Administrator',
username: 'root',
webUrl: 'link-to-author',
avatarUrl: 'link-to-avatar',
},
},
],
},
},
], ],
}, },
diffRefs: { diffRefs: {
......
export default [ export default [
{ {
id: 'note-id-1', id: 'note-id-1',
index: 1,
position: { position: {
height: 100, height: 100,
width: 100, width: 100,
x: 10, x: 10,
y: 15, y: 15,
}, },
author: {
name: 'John',
webUrl: 'link-to-john-profile',
},
createdAt: '2020-05-08T07:10:45Z',
userPermissions: { userPermissions: {
adminNote: true, adminNote: true,
}, },
discussion: { discussion: {
id: 'discussion-id-1', id: 'discussion-id-1',
}, },
resolved: false,
}, },
{ {
id: 'note-id-2', id: 'note-id-2',
index: 2,
position: { position: {
height: 50, height: 50,
width: 50, width: 50,
x: 25, x: 25,
y: 25, y: 25,
}, },
author: {
name: 'Mary',
webUrl: 'link-to-mary-profile',
},
createdAt: '2020-05-08T07:10:45Z',
userPermissions: { userPermissions: {
adminNote: true, adminNote: true,
}, },
discussion: { discussion: {
id: 'discussion-id-2', id: 'discussion-id-2',
}, },
resolved: true,
}, },
]; ];
...@@ -16,7 +16,7 @@ exports[`Design management design index page renders design index 1`] = ` ...@@ -16,7 +16,7 @@ exports[`Design management design index page renders design index 1`] = `
<!----> <!---->
<design-presentation-stub <design-presentation-stub
discussions="[object Object]" discussions="[object Object],[object Object]"
image="test.jpg" image="test.jpg"
imagename="test.jpg" imagename="test.jpg"
scale="1" scale="1"
...@@ -33,58 +33,84 @@ exports[`Design management design index page renders design index 1`] = ` ...@@ -33,58 +33,84 @@ exports[`Design management design index page renders design index 1`] = `
class="image-notes" class="image-notes"
> >
<h2 <h2
class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0" class="gl-font-weight-bold gl-mt-0"
> >
My precious issue My precious issue
</h2> </h2>
<a <a
class="text-tertiary text-decoration-none mb-3 d-block" class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url" href="full-issue-url"
> >
ull-issue-path ull-issue-path
</a> </a>
<participants-stub <participants-stub
class="mb-4" class="gl-mb-4"
numberoflessparticipants="7" numberoflessparticipants="7"
participants="[object Object]" participants="[object Object]"
/> />
<div <!---->
class="design-discussion-wrapper"
<design-discussion-stub
data-testid="unresolved-discussion"
designid="test"
discussion="[object Object]"
markdownpreviewpath="//preview_markdown?target_type=Issue"
noteableid="design-id"
/>
<gl-button-stub
category="tertiary"
class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
data-testid="resolved-comments"
icon="chevron-right"
id="resolved-comments"
size="medium"
variant="link"
> >
<div Resolved Comments (1)
class="badge badge-pill"
type="button" </gl-button-stub>
>
1 <gl-popover-stub
</div> container="popovercontainer"
cssclasses=""
placement="top"
show="true"
target="resolved-comments"
title="Resolved Comments"
>
<p>
Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below
</p>
<div <a
class="design-discussion bordered-box position-relative" href="#"
data-qa-selector="design_discussion_content" rel="noopener noreferrer"
target="_blank"
> >
<design-note-stub Learn more about resolving comments
class="" </a>
markdownpreviewpath="//preview_markdown?target_type=Issue" </gl-popover-stub>
note="[object Object]"
/> <gl-collapse-stub
class="gl-mt-3"
<div >
class="reply-wrapper" <design-discussion-stub
> data-testid="resolved-discussion"
<reply-placeholder-stub designid="test"
buttontext="Reply..." discussion="[object Object]"
class="qa-discussion-reply" markdownpreviewpath="//preview_markdown?target_type=Issue"
/> noteableid="design-id"
</div> />
</div> </gl-collapse-stub>
</div>
<!---->
</div> </div>
</div> </div>
`; `;
...@@ -152,33 +178,37 @@ exports[`Design management design index page with error GlAlert is rendered in c ...@@ -152,33 +178,37 @@ exports[`Design management design index page with error GlAlert is rendered in c
class="image-notes" class="image-notes"
> >
<h2 <h2
class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0" class="gl-font-weight-bold gl-mt-0"
> >
My precious issue My precious issue
</h2> </h2>
<a <a
class="text-tertiary text-decoration-none mb-3 d-block" class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url" href="full-issue-url"
> >
ull-issue-path ull-issue-path
</a> </a>
<participants-stub <participants-stub
class="mb-4" class="gl-mb-4"
numberoflessparticipants="7" numberoflessparticipants="7"
participants="[object Object]" participants="[object Object]"
/> />
<h2 <h2
class="new-discussion-disclaimer gl-font-base m-0" class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
data-testid="new-discussion-disclaimer"
> >
Click the image where you'd like to start a new discussion Click the image where you'd like to start a new discussion
</h2> </h2>
<!---->
</div> </div>
</div> </div>
`; `;
...@@ -4,11 +4,9 @@ import { GlAlert } from '@gitlab/ui'; ...@@ -4,11 +4,9 @@ import { GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash'; import createFlash from '~/flash';
import DesignIndex from '~/design_management/pages/design/index.vue'; import DesignIndex from '~/design_management/pages/design/index.vue';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import createImageDiffNoteMutation from '~/design_management/graphql/mutations/createImageDiffNote.mutation.graphql'; import createImageDiffNoteMutation from '~/design_management/graphql/mutations/createImageDiffNote.mutation.graphql';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
import design from '../../mock_data/design'; import design from '../../mock_data/design';
import mockResponseWithDesigns from '../../mock_data/designs'; import mockResponseWithDesigns from '../../mock_data/designs';
import mockResponseNoDesigns from '../../mock_data/no_designs'; import mockResponseNoDesigns from '../../mock_data/no_designs';
...@@ -62,20 +60,10 @@ describe('Design management design index page', () => { ...@@ -62,20 +60,10 @@ describe('Design management design index page', () => {
}, },
}; };
const updateActiveDiscussionMutationVariables = {
mutation: updateActiveDiscussionMutation,
variables: {
id: design.discussions.nodes[0].notes.nodes[0].id,
source: 'discussion',
},
};
const mutate = jest.fn().mockResolvedValue(); const mutate = jest.fn().mockResolvedValue();
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
const findDiscussionForm = () => wrapper.find(DesignReplyForm); const findDiscussionForm = () => wrapper.find(DesignReplyForm);
const findParticipants = () => wrapper.find(Participants); const findSidebar = () => wrapper.find(DesignSidebar);
const findDiscussionsWrapper = () => wrapper.find('.image-notes');
function createComponent(loading = false, data = {}) { function createComponent(loading = false, data = {}) {
const $apollo = { const $apollo = {
...@@ -94,7 +82,7 @@ describe('Design management design index page', () => { ...@@ -94,7 +82,7 @@ describe('Design management design index page', () => {
mocks: { $apollo }, mocks: { $apollo },
stubs: { stubs: {
ApolloMutation, ApolloMutation,
DesignDiscussion, DesignSidebar,
}, },
data() { data() {
return { return {
...@@ -145,63 +133,13 @@ describe('Design management design index page', () => { ...@@ -145,63 +133,13 @@ describe('Design management design index page', () => {
expect(wrapper.find(GlAlert).exists()).toBe(false); expect(wrapper.find(GlAlert).exists()).toBe(false);
}); });
it('renders participants', () => { it('passes correct props to sidebar component', () => {
createComponent(false, { design }); createComponent(false, { design });
expect(findParticipants().exists()).toBe(true); expect(findSidebar().props()).toEqual({
}); design,
markdownPreviewPath: '//preview_markdown?target_type=Issue',
it('passes the correct amount of participants to the Participants component', () => { resolvedDiscussionsExpanded: false,
createComponent(false, { design });
expect(findParticipants().props('participants')).toHaveLength(1);
});
describe('when has no discussions', () => {
beforeEach(() => {
createComponent(false, {
design: {
...design,
discussions: {
nodes: [],
},
},
});
});
it('does not render discussions', () => {
expect(findDiscussions().exists()).toBe(false);
});
it('renders a message about possibility to create a new discussion', () => {
expect(wrapper.find('.new-discussion-disclaimer').exists()).toBe(true);
});
});
describe('when has discussions', () => {
beforeEach(() => {
createComponent(false, { design });
});
it('renders correct amount of discussions', () => {
expect(findDiscussions()).toHaveLength(1);
});
it('sends a mutation to set an active discussion when clicking on a discussion', () => {
findDiscussions()
.at(0)
.trigger('click');
expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
});
it('sends a mutation to reset an active discussion when clicking outside of discussion', () => {
findDiscussionsWrapper().trigger('click');
expect(mutate).toHaveBeenCalledWith({
...updateActiveDiscussionMutationVariables,
variables: { id: undefined, source: 'discussion' },
});
}); });
}); });
......
...@@ -53,10 +53,10 @@ describe('extractDiscussions', () => { ...@@ -53,10 +53,10 @@ describe('extractDiscussions', () => {
it('discards the edges.node artifacts of GraphQL', () => { it('discards the edges.node artifacts of GraphQL', () => {
expect(extractDiscussions(discussions)).toEqual([ expect(extractDiscussions(discussions)).toEqual([
{ id: 1, notes: ['a'] }, { id: 1, notes: ['a'], index: 1 },
{ id: 2, notes: ['b'] }, { id: 2, notes: ['b'], index: 2 },
{ id: 3, notes: ['c'] }, { id: 3, notes: ['c'], index: 3 },
{ id: 4, notes: ['d'] }, { id: 4, notes: ['d'], index: 4 },
]); ]);
}); });
}); });
......
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