diff options
Diffstat (limited to 'app/assets/javascripts/notes')
8 files changed, 260 insertions, 147 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 554db102027..754c6e79ee4 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -321,10 +321,10 @@ Please check your network connection and try again.`; v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" /> - <ul + <div v-else-if="canCreateNote" class="notes notes-form timeline"> - <li class="timeline-entry"> + <div class="timeline-entry note-form"> <div class="timeline-entry-inner"> <div class="flash-container error-alert timeline-content"></div> <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" </form> </div> </div> - </li> - </ul> + </div> + </div> </div> </template> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 1f80f24e045..a4d76a70696 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,9 +1,6 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import resolveSvg from 'icons/_icon_resolve_discussion.svg'; -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 Icon from '~/vue_shared/components/icon.vue'; import { pluralize } from '../../lib/utils/text_utility'; import discussionNavigation from '../mixins/discussion_navigation'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -12,6 +9,9 @@ export default { directives: { tooltip, }, + components: { + Icon, + }, mixins: [discussionNavigation], computed: { ...mapGetters([ @@ -37,12 +37,6 @@ export default { return this.getNoteableData.create_issue_to_resolve_discussions_path; }, }, - created() { - this.resolveSvg = resolveSvg; - this.resolvedSvg = resolvedSvg; - this.mrIssueSvg = mrIssueSvg; - this.nextDiscussionSvg = nextDiscussionSvg; - }, methods: { ...mapActions(['expandDiscussion']), jumpToFirstUnresolvedDiscussion() { @@ -66,15 +60,9 @@ export default { <span :class="{ 'is-active': allResolved }" class="line-resolve-btn is-disabled" - type="button"> - <span - v-if="allResolved" - v-html="resolvedSvg" - ></span> - <span - v-else - v-html="resolveSvg" - ></span> + type="button" + > + <icon name="check-circle" /> </span> <span class="line-resolve-text"> {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved @@ -90,7 +78,7 @@ export default { :title="s__('Resolve all discussions in new issue')" data-container="body" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"> - <span v-html="mrIssueSvg"></span> + <icon name="issue-new" /> </a> </div> <div @@ -103,7 +91,7 @@ export default { data-container="body" class="btn btn-default discussion-next-btn" @click="jumpToFirstUnresolvedDiscussion"> - <span v-html="nextDiscussionSvg"></span> + <icon name="comment-next" /> </button> </div> </div> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 01cbe40f444..f7a61fbfcd4 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,12 +1,5 @@ <script> 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 tooltip from '~/vue_shared/directives/tooltip'; import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; @@ -110,15 +103,6 @@ export default { 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: { onEdit() { this.$emit('handleEdit'); @@ -152,12 +136,7 @@ export default { class="line-resolve-btn note-action-button" @click="onResolve"> <template v-if="!isResolving"> - <div - v-if="isResolved" - v-html="resolvedDiscussionSvg"></div> - <div - v-else - v-html="resolveDiscussionSvg"></div> + <icon name="check-circle" /> </template> <gl-loading-icon v-else @@ -179,18 +158,18 @@ export default { title="Add reaction" > <gl-loading-icon inline/> - <span - class="link-highlight award-control-icon-neutral" - v-html="emojiSmiling"> - </span> - <span - class="link-highlight award-control-icon-positive" - v-html="emojiSmiley"> - </span> - <span - class="link-highlight award-control-icon-super-positive" - v-html="emojiSmile"> - </span> + <icon + css-classes="link-highlight award-control-icon-neutral" + name="emoji_slightly_smiling_face" + /> + <icon + css-classes="link-highlight award-control-icon-positive" + name="emoji_smiley" + /> + <icon + css-classes="link-highlight award-control-icon-super-positive" + name="emoji_smiley" + /> </a> </div> <div @@ -204,10 +183,10 @@ export default { data-container="body" data-placement="bottom" @click="onEdit"> - <span - class="link-highlight" - v-html="editSvg"> - </span> + <icon + name="pencil" + css-classes="link-highlight" + /> </button> </div> <div @@ -240,10 +219,10 @@ export default { data-toggle="dropdown" data-container="body" data-placement="bottom"> - <span - class="icon" - v-html="ellipsisSvg"> - </span> + <icon + css-classes="icon" + name="ellipsis_v" + /> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index df7ab4502a6..401bcfabbe4 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -1,13 +1,14 @@ <script> import { mapActions, 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 Icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; import { glEmojiTag } from '../../emoji'; import tooltip from '../../vue_shared/directives/tooltip'; export default { + components: { + Icon, + }, directives: { tooltip, }, @@ -72,11 +73,6 @@ export default { return this.noteAuthorId === this.getUserData.id; }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - }, methods: { ...mapActions(['toggleAwardRequest']), getAwardHTML(name) { @@ -196,17 +192,14 @@ export default { data-boundary="viewport" data-placement="bottom" type="button"> - <span - class="award-control-icon award-control-icon-neutral" - v-html="emojiSmiling"> + <span class="award-control-icon award-control-icon-neutral"> + <icon name="emoji_slightly_smiling_face" /> </span> - <span - class="award-control-icon award-control-icon-positive" - v-html="emojiSmiley"> + <span class="award-control-icon award-control-icon-positive"> + <icon name="emoji_smiley" /> </span> - <span - class="award-control-icon award-control-icon-super-positive" - v-html="emojiSmile"> + <span class="award-control-icon award-control-icon-super-positive"> + <icon name="emoji_smiley" /> </span> <i aria-hidden="true" diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 7b6e7b72caf..dd7313d7b10 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -45,6 +45,9 @@ export default { noteTimestampLink() { return `#note_${this.noteId}`; }, + hasAuthor() { + return this.author && Object.keys(this.author).length; + }, }, methods: { ...mapActions(['setTargetNoteHash']), @@ -76,7 +79,7 @@ export default { </button> </div> <a - v-if="Object.keys(author).length" + v-if="hasAuthor" :href="author.path" > <span class="note-header-author-name">{{ author.name }}</span> @@ -92,9 +95,6 @@ export default { </span> <span class="note-headline-light"> <span class="note-headline-meta"> - <template v-if="actionText"> - {{ actionText }} - </template> <span class="system-note-message"> <slot></slot> </span> @@ -102,7 +102,9 @@ export default { v-if="createdAt" > <span class="system-note-separator"> - · + <template v-if="actionText"> + {{ actionText }} + </template> </span> <a :href="noteTimestampLink" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 07115ca07c4..a153edd0476 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,16 +1,16 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; -import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; -import systemNote from '~/vue_shared/components/notes/system_note.vue'; import { s__ } from '~/locale'; +import systemNote from '~/vue_shared/components/notes/system_note.vue'; +import icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteableNote from './noteable_note.vue'; import noteHeader from './note_header.vue'; +import toggleRepliesWidget from './toggle_replies_widget.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteEditedText from './note_edited_text.vue'; import noteForm from './note_form.vue'; @@ -26,6 +26,7 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { name: 'NoteableDiscussion', components: { + icon, noteableNote, diffWithNote, userAvatarLink, @@ -33,6 +34,7 @@ export default { noteSignedOutWidget, noteEditedText, noteForm, + toggleRepliesWidget, placeholderNote, placeholderSystemNote, systemNote, @@ -46,11 +48,6 @@ export default { type: Object, required: true, }, - renderHeader: { - type: Boolean, - required: false, - default: true, - }, renderDiffFile: { type: Boolean, required: false, @@ -72,6 +69,7 @@ export default { isReplying: false, isResolving: false, resolveAsThread: true, + isRepliesCollapsed: (!this.discussion.diff_discussion && this.discussion.resolved) || false, }; }, computed: { @@ -112,6 +110,15 @@ export default { newNotePath() { return this.getNoteableData.create_note_path; }, + hasReplies() { + return this.discussion.notes.length > 1; + }, + initialDiscussion() { + return this.discussion.notes.slice(0, 1)[0]; + }, + replies() { + return this.discussion.notes.slice(1); + }, lastUpdatedBy() { const { notes } = this.discussion; @@ -147,6 +154,12 @@ export default { return diffDiscussion && diffFile && this.renderDiffFile; }, + shouldGroupReplies() { + return !this.shouldRenderDiffs && !this.transformedDiscussion.diffDiscussion; + }, + shouldRenderHeader() { + return this.shouldRenderDiffs; + }, wrapperComponent() { return this.shouldRenderDiffs ? diffWithNote : 'div'; }, @@ -160,6 +173,22 @@ export default { wrapperClass() { return this.isDiffDiscussion ? '' : 'card discussion-wrapper'; }, + componentClassName() { + if (this.shouldRenderDiffs) { + if (!this.lastUpdatedAt && !this.discussion.resolved) { + return 'unresolved'; + } + } + + return ''; + }, + shouldShowDiscussions() { + const isExpanded = this.discussion.expanded; + const { diffDiscussion, resolved } = this.transformedDiscussion; + const isResolvedNonDiffDiscussion = !diffDiscussion && resolved; + + return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion; + }, }, watch: { isReplying() { @@ -173,10 +202,6 @@ export default { } }, }, - created() { - this.resolveDiscussionsSvg = resolveDiscussionsSvg; - this.nextDiscussionsSvg = nextDiscussionsSvg; - }, methods: { ...mapActions([ 'saveNote', @@ -207,6 +232,9 @@ export default { toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); }, + toggleReplies() { + this.isRepliesCollapsed = !this.isRepliesCollapsed; + }, showReplyForm() { this.isReplying = true; }, @@ -274,26 +302,29 @@ Please check your network connection and try again.`; </script> <template> - <li class="note note-discussion timeline-entry"> + <li + class="note note-discussion timeline-entry" + :class="componentClassName" + > <div class="timeline-entry-inner"> - <div class="timeline-icon"> - <user-avatar-link - v-if="author" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" - /> - </div> <div class="timeline-content"> <div :data-discussion-id="transformedDiscussion.discussion_id" class="discussion js-discussion-container" > <div - v-if="renderHeader" - class="discussion-header" + v-if="shouldRenderHeader" + class="discussion-header note-wrapper" > + <div class="timeline-icon"> + <user-avatar-link + v-if="author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" + /> + </div> <note-header :author="author" :created-at="transformedDiscussion.created_at" @@ -339,7 +370,7 @@ Please check your network connection and try again.`; /> </div> <div - v-if="discussion.expanded || alwaysExpanded" + v-if="shouldShowDiscussions" class="discussion-body"> <component :is="wrapperComponent" @@ -348,45 +379,70 @@ Please check your network connection and try again.`; > <div class="discussion-notes"> <ul class="notes"> - <component - :is="componentName(note)" - v-for="(note, index) in discussion.notes" - :key="note.id" - :note="componentData(note)" - @handleDeleteNote="deleteNoteHandler" - > - <slot - v-if="index === 0" - slot="avatar-badge" - name="avatar-badge" + <template v-if="shouldGroupReplies"> + <component + :is="componentName(initialDiscussion)" + :note="componentData(initialDiscussion)" + @handleDeleteNote="deleteNoteHandler" > - </slot> - </component> + <slot + slot="avatar-badge" + name="avatar-badge" + > + </slot> + </component> + <toggle-replies-widget + v-if="hasReplies" + :collapsed="isRepliesCollapsed" + :replies="replies" + @toggle="toggleReplies" + /> + <template v-if="!isRepliesCollapsed"> + <component + :is="componentName(note)" + v-for="note in replies" + :key="note.id" + :note="componentData(note)" + @handleDeleteNote="deleteNoteHandler" + /> + </template> + </template> + <template v-else> + <component + :is="componentName(note)" + v-for="(note, index) in discussion.notes" + :key="note.id" + :note="componentData(note)" + @handleDeleteNote="deleteNoteHandler" + > + <slot + v-if="index === 0" + slot="avatar-badge" + name="avatar-badge" + > + </slot> + </component> + </template> </ul> <div + v-if="!isRepliesCollapsed" :class="{ 'is-replying': isReplying }" class="discussion-reply-holder" > <template v-if="!isReplying && canReply"> - <div - class="btn-group d-flex discussion-with-resolve-btn" - role="group"> - <div - class="btn-group w-100" - role="group"> - <button - type="button" - class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply" - title="Add a reply" - @click="showReplyForm">Reply...</button> - </div> - <div - v-if="discussion.resolvable" - class="btn-group" - role="group"> + <div class="discussion-with-resolve-btn"> + <button + type="button" + class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply" + title="Add a reply" + @click="showReplyForm" + > + Reply... + </button> + <div v-if="discussion.resolvable"> <button type="button" - class="btn btn-default" + class="btn btn-default mx-sm-2" @click="resolveHandler()" > <i @@ -414,7 +470,7 @@ Please check your network connection and try again.`; btn-default discussion-create-issue-btn" data-container="body" > - <span v-html="resolveDiscussionsSvg"></span> + <icon name="issue-new" /> </a> </div> <div @@ -428,7 +484,7 @@ Please check your network connection and try again.`; data-container="body" @click="jumpToNextDiscussion" > - <span v-html="nextDiscussionsSvg"></span> + <icon name="comment-next" /> </button> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 40222ac4a80..e302a89ee95 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -173,7 +173,7 @@ export default { :class="classNameBindings" :data-award-url="note.toggle_award_path" :data-note-id="note.id" - class="note timeline-entry" + class="note timeline-entry note-wrapper" > <div class="timeline-entry-inner"> <div class="timeline-icon"> @@ -196,6 +196,7 @@ export default { :author="author" :created-at="note.created_at" :note-id="note.id" + action-text="commented" /> <note-actions :author-id="author.id" diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue new file mode 100644 index 00000000000..78ecbbb9247 --- /dev/null +++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue @@ -0,0 +1,94 @@ +<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> |