Commit 15e51945 authored by Mike Greiling's avatar Mike Greiling

Merge branch '_acet-discussion-redesign' into 'master'

Discussions redesign

Closes #29294

See merge request gitlab-org/gitlab-ce!22333
parents b5a79f15 cd5ddc4f
...@@ -76,7 +76,6 @@ export default { ...@@ -76,7 +76,6 @@ export default {
<noteable-discussion <noteable-discussion
v-show="isExpanded(discussion)" v-show="isExpanded(discussion)"
:discussion="discussion" :discussion="discussion"
:render-header="false"
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true" :always-expanded="true"
:discussions-by-diff-order="true" :discussions-by-diff-order="true"
......
...@@ -76,8 +76,9 @@ export default { ...@@ -76,8 +76,9 @@ export default {
:class="className" :class="className"
class="notes_holder" class="notes_holder"
> >
<td class="notes_line old"></td> <td
<td class="notes_content parallel old"> class="notes_content parallel old"
colspan="2">
<div <div
v-if="shouldRenderDiscussionsOnLeft" v-if="shouldRenderDiscussionsOnLeft"
class="content" class="content"
...@@ -95,8 +96,9 @@ export default { ...@@ -95,8 +96,9 @@ export default {
line-position="left" line-position="left"
/> />
</td> </td>
<td class="notes_line new"></td> <td
<td class="notes_content parallel new"> class="notes_content parallel new"
colspan="2">
<div <div
v-if="shouldRenderDiscussionsOnRight" v-if="shouldRenderDiscussionsOnRight"
class="content" class="content"
......
...@@ -321,10 +321,10 @@ Please check your network connection and try again.`; ...@@ -321,10 +321,10 @@ Please check your network connection and try again.`;
v-else-if="!canCreateNote" v-else-if="!canCreateNote"
:issuable-type="issuableTypeTitle" :issuable-type="issuableTypeTitle"
/> />
<ul <div
v-else-if="canCreateNote" v-else-if="canCreateNote"
class="notes notes-form timeline"> class="notes notes-form timeline">
<li class="timeline-entry"> <div class="timeline-entry note-form">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div> <div class="flash-container error-alert timeline-content"></div>
<div class="timeline-icon d-none d-sm-none d-md-block"> <div class="timeline-icon d-none d-sm-none d-md-block">
...@@ -462,7 +462,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -462,7 +462,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</form> </form>
</div> </div>
</div> </div>
</li> </div>
</ul> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg'; import Icon from '~/vue_shared/components/icon.vue';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation'; import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -12,6 +9,9 @@ export default { ...@@ -12,6 +9,9 @@ export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: {
Icon,
},
mixins: [discussionNavigation], mixins: [discussionNavigation],
computed: { computed: {
...mapGetters([ ...mapGetters([
...@@ -37,12 +37,6 @@ export default { ...@@ -37,12 +37,6 @@ export default {
return this.getNoteableData.create_issue_to_resolve_discussions_path; return this.getNoteableData.create_issue_to_resolve_discussions_path;
}, },
}, },
created() {
this.resolveSvg = resolveSvg;
this.resolvedSvg = resolvedSvg;
this.mrIssueSvg = mrIssueSvg;
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: { methods: {
...mapActions(['expandDiscussion']), ...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() { jumpToFirstUnresolvedDiscussion() {
...@@ -66,15 +60,9 @@ export default { ...@@ -66,15 +60,9 @@ export default {
<span <span
:class="{ 'is-active': allResolved }" :class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled" class="line-resolve-btn is-disabled"
type="button"> type="button"
<span >
v-if="allResolved" <icon name="check-circle" />
v-html="resolvedSvg"
></span>
<span
v-else
v-html="resolveSvg"
></span>
</span> </span>
<span class="line-resolve-text"> <span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
...@@ -90,7 +78,7 @@ export default { ...@@ -90,7 +78,7 @@ export default {
:title="s__('Resolve all discussions in new issue')" :title="s__('Resolve all discussions in new issue')"
data-container="body" data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"> class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
<span v-html="mrIssueSvg"></span> <icon name="issue-new" />
</a> </a>
</div> </div>
<div <div
...@@ -103,7 +91,7 @@ export default { ...@@ -103,7 +91,7 @@ export default {
data-container="body" data-container="body"
class="btn btn-default discussion-next-btn" class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion"> @click="jumpToFirstUnresolvedDiscussion">
<span v-html="nextDiscussionSvg"></span> <icon name="comment-next" />
</button> </button>
</div> </div>
</div> </div>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
...@@ -110,15 +103,6 @@ export default { ...@@ -110,15 +103,6 @@ export default {
return title; return title;
}, },
}, },
created() {
this.emojiSmiling = emojiSmiling;
this.emojiSmile = emojiSmile;
this.emojiSmiley = emojiSmiley;
this.editSvg = editSvg;
this.ellipsisSvg = ellipsisSvg;
this.resolveDiscussionSvg = resolveDiscussionSvg;
this.resolvedDiscussionSvg = resolvedDiscussionSvg;
},
methods: { methods: {
onEdit() { onEdit() {
this.$emit('handleEdit'); this.$emit('handleEdit');
...@@ -152,12 +136,7 @@ export default { ...@@ -152,12 +136,7 @@ export default {
class="line-resolve-btn note-action-button" class="line-resolve-btn note-action-button"
@click="onResolve"> @click="onResolve">
<template v-if="!isResolving"> <template v-if="!isResolving">
<div <icon name="check-circle" />
v-if="isResolved"
v-html="resolvedDiscussionSvg"></div>
<div
v-else
v-html="resolveDiscussionSvg"></div>
</template> </template>
<gl-loading-icon <gl-loading-icon
v-else v-else
...@@ -179,18 +158,18 @@ export default { ...@@ -179,18 +158,18 @@ export default {
title="Add reaction" title="Add reaction"
> >
<gl-loading-icon inline/> <gl-loading-icon inline/>
<span <icon
class="link-highlight award-control-icon-neutral" css-classes="link-highlight award-control-icon-neutral"
v-html="emojiSmiling"> name="emoji_slightly_smiling_face"
</span> />
<span <icon
class="link-highlight award-control-icon-positive" css-classes="link-highlight award-control-icon-positive"
v-html="emojiSmiley"> name="emoji_smiley"
</span> />
<span <icon
class="link-highlight award-control-icon-super-positive" css-classes="link-highlight award-control-icon-super-positive"
v-html="emojiSmile"> name="emoji_smiley"
</span> />
</a> </a>
</div> </div>
<div <div
...@@ -204,10 +183,10 @@ export default { ...@@ -204,10 +183,10 @@ export default {
data-container="body" data-container="body"
data-placement="bottom" data-placement="bottom"
@click="onEdit"> @click="onEdit">
<span <icon
class="link-highlight" name="pencil"
v-html="editSvg"> css-classes="link-highlight"
</span> />
</button> </button>
</div> </div>
<div <div
...@@ -240,10 +219,10 @@ export default { ...@@ -240,10 +219,10 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
data-container="body" data-container="body"
data-placement="bottom"> data-placement="bottom">
<span <icon
class="icon" css-classes="icon"
v-html="ellipsisSvg"> name="ellipsis_v"
</span> />
</button> </button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse"> <li v-if="canReportAsAbuse">
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import Icon from '~/vue_shared/components/icon.vue';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash'; import Flash from '../../flash';
import { glEmojiTag } from '../../emoji'; import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -72,11 +73,6 @@ export default { ...@@ -72,11 +73,6 @@ export default {
return this.noteAuthorId === this.getUserData.id; return this.noteAuthorId === this.getUserData.id;
}, },
}, },
created() {
this.emojiSmiling = emojiSmiling;
this.emojiSmile = emojiSmile;
this.emojiSmiley = emojiSmiley;
},
methods: { methods: {
...mapActions(['toggleAwardRequest']), ...mapActions(['toggleAwardRequest']),
getAwardHTML(name) { getAwardHTML(name) {
...@@ -196,17 +192,14 @@ export default { ...@@ -196,17 +192,14 @@ export default {
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
type="button"> type="button">
<span <span class="award-control-icon award-control-icon-neutral">
class="award-control-icon award-control-icon-neutral" <icon name="emoji_slightly_smiling_face" />
v-html="emojiSmiling">
</span> </span>
<span <span class="award-control-icon award-control-icon-positive">
class="award-control-icon award-control-icon-positive" <icon name="emoji_smiley" />
v-html="emojiSmiley">
</span> </span>
<span <span class="award-control-icon award-control-icon-super-positive">
class="award-control-icon award-control-icon-super-positive" <icon name="emoji_smiley" />
v-html="emojiSmile">
</span> </span>
<i <i
aria-hidden="true" aria-hidden="true"
......
...@@ -45,6 +45,9 @@ export default { ...@@ -45,6 +45,9 @@ export default {
noteTimestampLink() { noteTimestampLink() {
return `#note_${this.noteId}`; return `#note_${this.noteId}`;
}, },
hasAuthor() {
return this.author && Object.keys(this.author).length;
},
}, },
methods: { methods: {
...mapActions(['setTargetNoteHash']), ...mapActions(['setTargetNoteHash']),
...@@ -76,7 +79,7 @@ export default { ...@@ -76,7 +79,7 @@ export default {
</button> </button>
</div> </div>
<a <a
v-if="Object.keys(author).length" v-if="hasAuthor"
:href="author.path" :href="author.path"
> >
<span class="note-header-author-name">{{ author.name }}</span> <span class="note-header-author-name">{{ author.name }}</span>
...@@ -92,9 +95,6 @@ export default { ...@@ -92,9 +95,6 @@ export default {
</span> </span>
<span class="note-headline-light"> <span class="note-headline-light">
<span class="note-headline-meta"> <span class="note-headline-meta">
<template v-if="actionText">
{{ actionText }}
</template>
<span class="system-note-message"> <span class="system-note-message">
<slot></slot> <slot></slot>
</span> </span>
...@@ -102,7 +102,9 @@ export default { ...@@ -102,7 +102,9 @@ export default {
v-if="createdAt" v-if="createdAt"
> >
<span class="system-note-separator"> <span class="system-note-separator">
&middot; <template v-if="actionText">
{{ actionText }}
</template>
</span> </span>
<a <a
:href="noteTimestampLink" :href="noteTimestampLink"
......
...@@ -173,7 +173,7 @@ export default { ...@@ -173,7 +173,7 @@ export default {
:class="classNameBindings" :class="classNameBindings"
:data-award-url="note.toggle_award_path" :data-award-url="note.toggle_award_path"
:data-note-id="note.id" :data-note-id="note.id"
class="note timeline-entry" class="note timeline-entry note-wrapper"
> >
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div class="timeline-icon"> <div class="timeline-icon">
...@@ -196,6 +196,7 @@ export default { ...@@ -196,6 +196,7 @@ export default {
:author="author" :author="author"
:created-at="note.created_at" :created-at="note.created_at"
:note-id="note.id" :note-id="note.id"
action-text="commented"
/> />
<note-actions <note-actions
:author-id="author.id" :author-id="author.id"
......
<script>
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
Icon,
UserAvatarLink,
TimeAgoTooltip,
},
props: {
collapsed: {
type: Boolean,
required: true,
},
replies: {
type: Array,
required: true,
},
},
computed: {
lastReply() {
return this.replies[this.replies.length - 1];
},
uniqueAuthors() {
const authors = this.replies.map(reply => reply.author || {});
return _.uniq(authors, author => author.username);
},
className() {
return this.collapsed ? 'collapsed' : 'expanded';
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<template>
<li
:class="className"
class="replies-toggle"
>
<template v-if="collapsed">
<icon
name="chevron-right"
@click.native="toggle"
/>
<div>
<user-avatar-link
v-for="author in uniqueAuthors"
:key="author.username"
:link-href="author.path"
:img-alt="author.name"
:img-src="author.avatar_url"
:img-size="26"
:tooltip-text="author.name"
tooltip-placement="bottom"
/>
</div>
<button
class="btn btn-link js-replies-text"
type="button"
@click="toggle"
>
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</button>
{{ __('Last reply by') }}
<a
:href="lastReply.author.path"
class="btn btn-link author-link"
>
{{ lastReply.author.name }}
</a>
<time-ago-tooltip
:time="lastReply.created_at"
tooltip-placement="bottom"
/>
</template>
<span
v-else
class="collapse-replies-btn js-collapse-replies"
@click="toggle"
>
<icon name="chevron-down" />
{{ s__('Notes|Collapse replies') }}
</span>
</li>
</template>
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
</script> </script>
<template> <template>
<li class="timeline-entry note"> <li class="timeline-entry note note-wrapper">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div class="timeline-icon"> <div class="timeline-icon">
</div> </div>
......
...@@ -76,7 +76,7 @@ export default { ...@@ -76,7 +76,7 @@ export default {
<li <li
:id="noteAnchorId" :id="noteAnchorId"
:class="{ target: isTargetNote }" :class="{ target: isTargetNote }"
class="note system-note timeline-entry"> class="note system-note timeline-entry note-wrapper">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div <div
class="timeline-icon" class="timeline-icon"
......
...@@ -148,10 +148,7 @@ ...@@ -148,10 +148,7 @@
.award-control-icon svg { .award-control-icon svg {
background: $award-emoji-positive-add-bg; background: $award-emoji-positive-add-bg;
fill: $award-emoji-positive-add-lines;
path {
fill: $award-emoji-positive-add-lines;
}
} }
.award-control-icon-neutral { .award-control-icon-neutral {
......
...@@ -222,6 +222,25 @@ ...@@ -222,6 +222,25 @@
} }
} }
&.btn-text-field {
width: 100%;
text-align: left;
padding: 6px 16px;
border-color: $border-color;
color: $gray-darkest;
background-color: $gray-light;
&:hover,
&:active,
&:focus {
cursor: text;
box-shadow: none;
border-color: lighten($blue-300, 20%);
color: $gray-darkest;
background-color: $gray-light;
}
}
&.dot-highlight::after { &.dot-highlight::after {
content: ''; content: '';
background-color: $blue-500; background-color: $blue-500;
...@@ -339,25 +358,6 @@ ...@@ -339,25 +358,6 @@
} }
} }
.btn-text-field {
width: 100%;
text-align: left;
padding: 6px 16px;
border-color: $border-color;
color: $gray-darkest;
background-color: $gray-light;
&:hover,
&:active,
&:focus {
cursor: text;
box-shadow: none;
border-color: lighten($blue-300, 20%);
color: $gray-darkest;
background-color: $gray-light;
}
}
.btn-build { .btn-build {
margin-left: 10px; margin-left: 10px;
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
text-align: left; text-align: left;
padding: 10px $gl-padding; padding: 10px $gl-padding;
word-wrap: break-word; word-wrap: break-word;
border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear { &.file-title-clear {
padding-left: 0; padding-left: 0;
......
.timeline { .timeline {
@include basic-list;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none;
&::before { &::before {
@include notes-media('max', map-get($grid-breakpoints, sm)) { @include notes-media('max', map-get($grid-breakpoints, sm)) {
...@@ -26,10 +26,8 @@ ...@@ -26,10 +26,8 @@
} }
.timeline-entry { .timeline-entry {
border-color: $white-normal;
color: $gl-text-color; color: $gl-text-color;
border-bottom: 1px solid $border-white-light; background-color: $white-light;
background: $white-light;
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
......
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
table-layout: fixed; table-layout: fixed;
border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-line-num { .diff-line-num {
width: 50px; width: 50px;
...@@ -859,7 +860,7 @@ ...@@ -859,7 +860,7 @@
} }
.diff-file .note-container > .new-note, .diff-file .note-container > .new-note,
.note-container .discussion-notes { .note-container .discussion-notes.diff-discussions {
margin-left: 100px; margin-left: 100px;
border-left: 1px solid $white-normal; border-left: 1px solid $white-normal;
} }
......
...@@ -239,6 +239,7 @@ ...@@ -239,6 +239,7 @@
.discussion-reply-holder { .discussion-reply-holder {
background-color: $white-light; background-color: $white-light;
padding: 10px 16px; padding: 10px 16px;
border-radius: 0 0 $border-radius-default $border-radius-default;
&.is-replying { &.is-replying {
padding-bottom: $gl-padding; padding-bottom: $gl-padding;
...@@ -247,10 +248,15 @@ ...@@ -247,10 +248,15 @@
} }
.discussion-with-resolve-btn { .discussion-with-resolve-btn {
@include media-breakpoint-up(sm) {
display: flex;
}
.discussion-actions { .discussion-actions {
display: table; display: table;
.btn-default path { svg {
fill: $gray-darkest; fill: $gray-darkest;
} }
...@@ -270,6 +276,12 @@ ...@@ -270,6 +276,12 @@
.btn { .btn {
width: 100%; width: 100%;
} }
.btn-text-field {
@include media-breakpoint-down(xs) {
margin-bottom: $gl-padding-8;
}
}
} }
.discussion-notes-count { .discussion-notes-count {
......
/** $system-note-icon-size: 32px;
* Notes $system-note-svg-size: 16px;
*/ $note-form-margin-left: 70px;
@-webkit-keyframes targe3-note { @mixin vertical-line($left) {
from { &::before {
background: $note-targe3-outside; content: '';
border-left: 2px solid $theme-gray-100;
position: absolute;
top: 0;
bottom: 0;
left: $left;
} }
}
50% { .note-wrapper {
background: $note-targe3-inside; padding: $gl-padding;
} }
.issuable-discussion {
.notes.timeline > .timeline-entry {
border: 1px solid $border-color;
border-radius: $border-radius-default;
margin: $gl-padding 0;
&.system-note,
&.note-form {
border: 0;
}
&.note-form {
margin-left: 0;
to { @include notes-media('min', map-get($grid-breakpoints, md)) {
background: $note-targe3-outside; margin-left: $note-form-margin-left;
}
.timeline-icon {
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: -$note-icon-gutter-width;
}
}
.timeline-content {
margin-left: 0;
}
}
.notes_content {
border: 0;
border-top: 1px solid $border-color;
}
} }
} }
ul.notes { .main-notes-list {
@include vertical-line(39px);
}
.notes {
display: block; display: block;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative;
> .note-discussion {
.card {
border: 0;
}
li.note {
border-bottom: 1px solid $border-color;
&:first-child {
border-radius: $border-radius-default $border-radius-default 0 0;
}
}
}
.replies-toggle {
background-color: $gray-light;
padding: $gl-padding-8 $gl-padding;
.collapse-replies-btn:hover {
color: $blue-600;
}
&.expanded {
border-bottom: 1px solid $border-color;
span {
cursor: pointer;
}
svg {
position: relative;
top: 3px;
}
}
&.collapsed {
color: $gl-text-color-secondary;
svg {
float: left;
position: relative;
top: $gl-padding-4;
margin-right: $gl-padding-8;
cursor: pointer;
}
img {
margin: -2px 4px 0 0;
}
.author-link {
color: $gl-text-color;
}
}
.user-avatar-link {
&:last-child img {
margin-right: $gl-padding-8;
}
}
.btn-link {
border: 0;
vertical-align: baseline;
}
}
.note-created-ago, .note-created-ago,
.note-updated-at { .note-updated-at {
...@@ -28,8 +137,6 @@ ul.notes { ...@@ -28,8 +137,6 @@ ul.notes {
} }
.discussion-body { .discussion-body {
padding-top: 8px;
.card { .card {
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -46,21 +153,10 @@ ul.notes { ...@@ -46,21 +153,10 @@ ul.notes {
} }
> li { > li {
// .timeline-entry
padding: 0;
display: block; display: block;
position: relative; position: relative;
border-bottom: 0; border-bottom: 0;
@include notes-media('min', map-get($grid-breakpoints, sm)) {
padding-left: $note-icon-gutter-width;
}
.timeline-entry-inner {
padding: $gl-padding $gl-btn-padding;
border-bottom: 1px solid $white-normal;
}
&:target, &:target,
&.target { &.target {
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
...@@ -75,23 +171,10 @@ ul.notes { ...@@ -75,23 +171,10 @@ ul.notes {
} }
} }
.timeline-icon {
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: -$note-icon-gutter-width;
}
}
.timeline-content {
margin-left: $note-icon-gutter-width;
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 0;
}
}
&.being-posted { &.being-posted {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
padding: $gl-padding;
.dummy-avatar { .dummy-avatar {
background-color: $gl-gray-200; background-color: $gl-gray-200;
...@@ -104,12 +187,6 @@ ul.notes { ...@@ -104,12 +187,6 @@ ul.notes {
} }
} }
&.note-discussion {
.timeline-entry-inner {
padding: $gl-padding 10px;
}
}
.editing-spinner { .editing-spinner {
display: none; display: none;
} }
...@@ -191,8 +268,9 @@ ul.notes { ...@@ -191,8 +268,9 @@ ul.notes {
} }
.system-note { .system-note {
font-size: 14px; padding: 6px $gl-padding-24;
clear: both; margin: $gl-padding-24 0;
background-color: transparent;
.note-header-info { .note-header-info {
padding-bottom: 0; padding-bottom: 0;
...@@ -225,17 +303,21 @@ ul.notes { ...@@ -225,17 +303,21 @@ ul.notes {
.timeline-icon { .timeline-icon {
float: left; float: left;
display: flex;
@include notes-media('min', map-get($grid-breakpoints, sm)) { align-items: center;
margin-left: 0; background-color: $white-light;
width: auto; width: $system-note-icon-size;
} height: $system-note-icon-size;
border: 1px solid $border-color;
border-radius: $system-note-icon-size;
margin: -6px $gl-padding 0 0;
svg { svg {
width: 16px; width: $system-note-svg-size;
height: 16px; height: $system-note-svg-size;
fill: $gray-darkest; fill: $gray-darkest;
margin-top: 2px; display: block;
margin: 0 auto;
} }
} }
...@@ -302,10 +384,17 @@ ul.notes { ...@@ -302,10 +384,17 @@ ul.notes {
.discussion-body .diff-file { .discussion-body .diff-file {
.file-title { .file-title {
cursor: default; cursor: default;
line-height: 42px;
padding: 0 $gl-padding;
border-top: 1px solid $border-color;
&:hover { &:hover {
background-color: $gray-light; background-color: $gray-light;
} }
.btn-clipboard {
top: 10px;
}
} }
.line_content { .line_content {
...@@ -320,6 +409,23 @@ ul.notes { ...@@ -320,6 +409,23 @@ ul.notes {
} }
} }
.discussion-notes {
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
}
&:not(:last-child) {
border-bottom: 1px solid $white-normal;
margin-bottom: 20px;
}
.system-note {
margin: 0;
padding: $gl-padding;
}
}
// Merge request notes in diffs // Merge request notes in diffs
// Diff is inline // Diff is inline
.notes_content .note-header .note-headline-light { .notes_content .note-header .note-headline-light {
...@@ -335,7 +441,6 @@ ul.notes { ...@@ -335,7 +441,6 @@ ul.notes {
border-left: 0; border-left: 0;
&.notes_content { &.notes_content {
background-color: $gray-light;
border-width: 1px 0; border-width: 1px 0;
padding: 0; padding: 0;
vertical-align: top; vertical-align: top;
...@@ -349,18 +454,6 @@ ul.notes { ...@@ -349,18 +454,6 @@ ul.notes {
} }
} }
.discussion-notes {
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
}
&:not(:last-child) {
border-bottom: 1px solid $white-normal;
margin-bottom: 20px;
}
}
.notes { .notes {
background-color: $white-light; background-color: $white-light;
} }
...@@ -374,6 +467,30 @@ ul.notes { ...@@ -374,6 +467,30 @@ ul.notes {
} }
} }
.diffs {
.discussion-notes {
margin-left: 0;
border-left: 0;
.notes {
position: relative;
@include vertical-line(52px);
}
}
.note-wrapper {
margin: $gl-padding;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.discussion-reply-holder {
border-radius: 0 0 $border-radius-default $border-radius-default;
border-top: 1px solid $border-color;
position: relative;
}
}
.discussion-header, .discussion-header,
.note-header-info { .note-header-info {
a { a {
...@@ -399,7 +516,17 @@ ul.notes { ...@@ -399,7 +516,17 @@ ul.notes {
} }
.discussion-header { .discussion-header {
font-size: 14px; min-height: 72px;
.note-header-info {
padding-bottom: 0;
}
}
.unresolved {
.note-header-info {
margin-top: $gl-padding-8;
}
} }
.note-header { .note-header {
...@@ -409,7 +536,7 @@ ul.notes { ...@@ -409,7 +536,7 @@ ul.notes {
.note-header-info { .note-header-info {
min-width: 0; min-width: 0;
padding-bottom: 8px; padding-bottom: $gl-padding-8;
&.discussion { &.discussion {
padding-bottom: 0; padding-bottom: 0;
...@@ -471,9 +598,18 @@ ul.notes { ...@@ -471,9 +598,18 @@ ul.notes {
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
@include media-breakpoint-down(xs) {
width: 100%;
margin: $gl-padding-8 0;
}
.btn-group > .discussion-next-btn { .btn-group > .discussion-next-btn {
margin-left: -1px; margin-left: -1px;
} }
svg {
height: 15px;
}
} }
.note-actions { .note-actions {
...@@ -585,19 +721,6 @@ ul.notes { ...@@ -585,19 +721,6 @@ ul.notes {
z-index: 10; z-index: 10;
} }
.discussion-body,
.diff-file {
.notes .note {
border-bottom: 1px solid $white-normal;
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
border-bottom: 0;
}
}
}
.disabled-comment { .disabled-comment {
background-color: $gray-light; background-color: $gray-light;
border-radius: $border-radius-base; border-radius: $border-radius-base;
...@@ -634,7 +757,7 @@ ul.notes { ...@@ -634,7 +757,7 @@ ul.notes {
} }
.btn { .btn {
svg path { svg {
fill: $gray-darkest; fill: $gray-darkest;
} }
...@@ -659,7 +782,7 @@ ul.notes { ...@@ -659,7 +782,7 @@ ul.notes {
.line-resolve-all { .line-resolve-all {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
padding: 5px 10px 6px; padding: 6px 10px;
background-color: $gray-light; background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
= render 'shared/notes/edit_form', project: @project = render 'shared/notes/edit_form', project: @project
- if can_create_note? - if can_create_note?
%ul.notes.notes-form.timeline .notes.notes-form.timeline
%li.timeline-entry .timeline-entry
.timeline-entry-inner .timeline-entry-inner
.flash-container.timeline-content .flash-container.timeline-content
......
...@@ -3591,6 +3591,9 @@ msgstr "" ...@@ -3591,6 +3591,9 @@ msgstr ""
msgid "Last edited by %{name}" msgid "Last edited by %{name}"
msgstr "" msgstr ""
msgid "Last reply by"
msgstr ""
msgid "Last update" msgid "Last update"
msgstr "" msgstr ""
...@@ -4192,6 +4195,9 @@ msgstr "" ...@@ -4192,6 +4195,9 @@ msgstr ""
msgid "Notes|Are you sure you want to cancel creating this comment?" msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr "" msgstr ""
msgid "Notes|Collapse replies"
msgstr ""
msgid "Notes|Show all activity" msgid "Notes|Show all activity"
msgstr "" msgstr ""
...@@ -7568,6 +7574,11 @@ msgstr "" ...@@ -7568,6 +7574,11 @@ msgstr ""
msgid "remove due date" msgid "remove due date"
msgstr "" msgstr ""
msgid "reply"
msgid_plural "replies"
msgstr[0] ""
msgstr[1] ""
msgid "source" msgid "source"
msgstr "" msgstr ""
......
...@@ -3,6 +3,7 @@ import createStore from '~/notes/stores'; ...@@ -3,6 +3,7 @@ import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
...@@ -33,9 +34,20 @@ describe('noteable_discussion component', () => { ...@@ -33,9 +34,20 @@ describe('noteable_discussion component', () => {
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
}); });
it('should not render discussion header for non diff discussions', () => {
expect(vm.$el.querySelector('.discussion-header')).toBeNull();
});
it('should render discussion header', () => { it('should render discussion header', () => {
expect(vm.$el.querySelector('.discussion-header')).not.toBeNull(); const discussion = { ...discussionMock };
expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length); discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
const diffDiscussionVm = new Component({
store,
propsData: { discussion },
}).$mount();
expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull();
}); });
describe('actions', () => { describe('actions', () => {
......
import Vue from 'vue';
import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { note } from '../mock_data';
const deepCloneObject = obj => JSON.parse(JSON.stringify(obj));
describe('toggle replies widget for notes', () => {
let vm;
let ToggleRepliesWidget;
const noteFromOtherUser = deepCloneObject(note);
noteFromOtherUser.author.username = 'fatihacet';
const noteFromAnotherUser = deepCloneObject(note);
noteFromAnotherUser.author.username = 'mgreiling';
noteFromAnotherUser.author.name = 'Mike Greiling';
const replies = [note, note, note, noteFromOtherUser, noteFromAnotherUser];
beforeEach(() => {
ToggleRepliesWidget = Vue.extend(toggleRepliesWidget);
});
afterEach(() => {
vm.$destroy();
});
describe('collapsed state', () => {
beforeEach(() => {
vm = mountComponent(ToggleRepliesWidget, {
replies,
collapsed: true,
});
});
it('should render the collapsed', () => {
const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
expect(vm.$el.classList.contains('collapsed')).toEqual(true);
expect(vm.$el.querySelectorAll('.user-avatar-link').length).toEqual(3);
expect(vm.$el.querySelector('time')).not.toBeNull();
expect(vmTextContent).toContain('5 replies');
expect(vmTextContent).toContain(`Last reply by ${noteFromAnotherUser.author.name}`);
});
it('should emit toggle event when the replies text clicked', () => {
const spy = spyOn(vm, '$emit');
vm.$el.querySelector('.js-replies-text').click();
expect(spy).toHaveBeenCalledWith('toggle');
});
});
describe('expanded state', () => {
beforeEach(() => {
vm = mountComponent(ToggleRepliesWidget, {
replies,
collapsed: false,
});
});
it('should render expanded state', () => {
const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
expect(vm.$el.querySelector('.collapse-replies-btn')).not.toBeNull();
expect(vmTextContent).toContain('Collapse replies');
});
it('should emit toggle event when the collapse replies text called', () => {
const spy = spyOn(vm, '$emit');
vm.$el.querySelector('.js-collapse-replies').click();
expect(spy).toHaveBeenCalledWith('toggle');
});
});
});
...@@ -150,17 +150,25 @@ shared_examples 'discussion comments' do |resource_name| ...@@ -150,17 +150,25 @@ shared_examples 'discussion comments' do |resource_name|
end end
if resource_name == 'merge request' if resource_name == 'merge request'
let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] } let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
let(:reply_id) { find("#{comments_selector} .note:last-child", match: :first)['data-note-id'] }
it 'shows resolved discussion when toggled' do it 'shows resolved discussion when toggled' do
find("#{comments_selector} .js-vue-discussion-reply").click
find("#{comments_selector} .note-textarea").send_keys('a')
click_button "Comment"
wait_for_requests
click_button "Resolve discussion" click_button "Resolve discussion"
wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true) expect(page).to have_selector(".note-row-#{note_id}", visible: true)
refresh refresh
click_button "Toggle discussion" click_button "1 reply"
expect(page).to have_selector(".note-row-#{note_id}", visible: true) expect(page).to have_selector(".note-row-#{reply_id}", visible: true)
end end
end end
end end
......
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