diff options
Diffstat (limited to 'app/assets/javascripts/notes/components')
17 files changed, 640 insertions, 317 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 1d6cb9485f7..075c28e8d07 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -11,6 +11,7 @@ import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase, + slugifyWithUnderscore, } from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; @@ -115,8 +116,11 @@ export default { author() { return this.getUserData; }, - canUpdateIssue() { - return this.getNoteableData.current_user.can_update; + canToggleIssueState() { + return ( + this.getNoteableData.current_user.can_update && + this.getNoteableData.state !== constants.MERGED + ); }, endpoint() { return this.getNoteableData.create_note_path; @@ -126,6 +130,9 @@ export default { ? 'merge request' : 'issue'; }, + trackingLabel() { + return slugifyWithUnderscore(`${this.commentButtonTitle} button`); + }, }, watch: { note(newNote) { @@ -330,6 +337,8 @@ Please check your network connection and try again.`; v-if="hasWarning(getNoteableData)" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" + :locked-issue-docs-path="lockedIssueDocsPath" + :confidential-issue-docs-path="confidentialIssueDocsPath" /> <markdown-field @@ -344,6 +353,7 @@ Please check your network connection and try again.`; ref="textarea" slot="textarea" v-model="note" + dir="auto" :disabled="isSubmitting" name="note[note]" class="note-textarea js-vue-comment-form js-note-text @@ -367,6 +377,8 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" type="submit" + :data-track-label="trackingLabel" + data-track-event="click_button" @click.prevent="handleSave()" > {{ __(commentButtonTitle) }} @@ -415,7 +427,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" </div> <loading-button - v-if="canUpdateIssue" + v-if="canToggleIssueState" :loading="isToggleStateButtonLoading" :container-class="[ actionButtonClassNames, diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index d8947e8ca50..b95835ed10a 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -72,8 +72,8 @@ export default { :can-current-user-fork="false" :expanded="!discussion.diff_file.viewer.collapsed" /> - <div v-if="isTextFile" :class="$options.userColorSchemeClass" class="diff-content code"> - <table> + <div v-if="isTextFile" class="diff-content"> + <table class="code js-syntax-highlight" :class="$options.userColorSchemeClass"> <template v-if="hasTruncatedDiffLines"> <tr v-for="line in discussion.truncated_diff_lines" @@ -81,8 +81,8 @@ export default { :key="line.line_code" class="line_holder" > - <td class="diff-line-num old_line">{{ line.old_line }}</td> - <td class="diff-line-num new_line">{{ line.new_line }}</td> + <td :class="line.type" class="diff-line-num old_line">{{ line.old_line }}</td> + <td :class="line.type" class="diff-line-num new_line">{{ line.new_line }}</td> <td :class="line.type" class="line_content" v-html="line.rich_text"></td> </tr> </template> @@ -105,7 +105,7 @@ export default { </td> </tr> <tr class="notes_holder"> - <td class="notes_content" colspan="3"><slot></slot></td> + <td class="notes-content" colspan="3"><slot></slot></td> </tr> </table> </div> diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue new file mode 100644 index 00000000000..22cca756ef6 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -0,0 +1,58 @@ +<script> +import ReplyPlaceholder from './discussion_reply_placeholder.vue'; +import ResolveDiscussionButton from './discussion_resolve_button.vue'; +import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue'; +import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; + +export default { + name: 'DiscussionActions', + components: { + ReplyPlaceholder, + ResolveDiscussionButton, + ResolveWithIssueButton, + JumpToNextDiscussionButton, + }, + props: { + discussion: { + type: Object, + required: true, + }, + isResolving: { + type: Boolean, + required: true, + }, + resolveButtonTitle: { + type: String, + required: true, + }, + resolveWithIssuePath: { + type: String, + required: false, + default: '', + }, + shouldShowJumpToNextDiscussion: { + type: Boolean, + required: true, + }, + }, +}; +</script> + +<template> + <div class="discussion-with-resolve-btn"> + <reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" /> + <resolve-discussion-button + v-if="discussion.resolvable" + :is-resolving="isResolving" + :button-title="resolveButtonTitle" + @onClick="$emit('resolve')" + /> + <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group"> + <resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" /> + <jump-to-next-discussion-button + v-if="shouldShowJumpToNextDiscussion" + @onClick="$emit('jumpToNextDiscussion')" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index c7cfc0f0f3b..efd84f5722c 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -49,22 +49,26 @@ export default { </script> <template> - <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container prepend-top-8"> - <div> + <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container full-width-mobile"> + <div class="full-width-mobile d-flex d-sm-block"> <div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all"> <span :class="{ 'is-active': allResolved }" class="line-resolve-btn is-disabled" type="button" > - <icon name="check-circle" /> + <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" /> </span> <span class="line-resolve-text"> {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }} {{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }} </span> </div> - <div v-if="resolveAllDiscussionsIssuePath && !allResolved" class="btn-group" role="group"> + <div + v-if="resolveAllDiscussionsIssuePath && !allResolved" + class="btn-group btn-group-sm" + role="group" + > <a v-gl-tooltip :href="resolveAllDiscussionsIssuePath" @@ -74,7 +78,7 @@ export default { <icon name="issue-new" /> </a> </div> - <div v-if="isLoggedIn && !allResolved" class="btn-group" role="group"> + <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> <button v-gl-tooltip title="Jump to first unresolved discussion" diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index e03d6e9cd02..eb3fbbe1385 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -7,7 +7,9 @@ import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE, DISCUSSION_TAB_LABEL, + DISCUSSION_FILTER_TYPES, } from '../constants'; +import notesEventHub from '../event_hub'; export default { components: { @@ -20,7 +22,7 @@ export default { }, selectedValue: { type: Number, - default: null, + default: DISCUSSION_FILTERS_DEFAULT_VALUE, required: false, }, }, @@ -46,6 +48,7 @@ export default { this.toggleFilters(currentTab); } + notesEventHub.$on('dropdownSelect', this.selectFilter); window.addEventListener('hashchange', this.handleLocationHash); this.handleLocationHash(); }, @@ -53,6 +56,7 @@ export default { this.toggleCommentsForm(); }, destroyed() { + notesEventHub.$off('dropdownSelect', this.selectFilter); window.removeEventListener('hashchange', this.handleLocationHash); }, methods: { @@ -86,28 +90,44 @@ export default { this.setTargetNoteHash(hash); } }, + filterType(value) { + if (value === 0) { + return DISCUSSION_FILTER_TYPES.ALL; + } else if (value === 1) { + return DISCUSSION_FILTER_TYPES.COMMENTS; + } + return DISCUSSION_FILTER_TYPES.HISTORY; + }, }, }; </script> <template> - <div v-if="displayFilters" class="discussion-filter-container d-inline-block align-bottom"> + <div + v-if="displayFilters" + class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom full-width-mobile" + > <button id="discussion-filter-dropdown" ref="dropdownToggle" - class="btn btn-default qa-discussion-filter" + class="btn btn-sm qa-discussion-filter" data-toggle="dropdown" aria-expanded="false" > {{ currentFilter.title }} <icon name="chevron-down" /> </button> <div + ref="dropdownMenu" class="dropdown-menu dropdown-menu-selectable dropdown-menu-right" aria-labelledby="discussion-filter-dropdown" > <div class="dropdown-content"> <ul> - <li v-for="filter in filters" :key="filter.value"> + <li + v-for="filter in filters" + :key="filter.value" + :data-filter-type="filterType(filter.value)" + > <button :class="{ 'is-active': filter.value === currentValue }" class="qa-filter-options" diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue new file mode 100644 index 00000000000..889731df180 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue @@ -0,0 +1,52 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import { __, sprintf } from '~/locale'; + +import notesEventHub from '../event_hub'; + +export default { + components: { + GlButton, + Icon, + }, + computed: { + timelineContent() { + return sprintf( + __( + "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options.", + ), + { + startTag: `<b>`, + endTag: `</b>`, + }, + false, + ); + }, + }, + methods: { + selectFilter(value) { + notesEventHub.$emit('dropdownSelect', value); + }, + }, +}; +</script> + +<template> + <li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"> + <div class="timeline-icon d-none d-lg-flex"> + <icon name="comment" /> + </div> + <div class="timeline-content"> + <div v-html="timelineContent"></div> + <div class="discussion-filter-actions mt-2"> + <gl-button variant="default" @click="selectFilter(0)"> + {{ __('Show all activity') }} + </gl-button> + <gl-button variant="default" @click="selectFilter(1)"> + {{ __('Show comments only') }} + </gl-button> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index c469a6b7bcd..53f509185a8 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -1,12 +1,24 @@ <script> +import { GlLink } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; +import { __, sprintf } from '~/locale'; import Issuable from '~/vue_shared/mixins/issuable'; +import issuableStateMixin from '../mixins/issuable_state'; export default { components: { Icon, + GlLink, + }, + mixins: [Issuable, issuableStateMixin], + computed: { + lockedIssueWarning() { + return sprintf( + __('This %{issuableDisplayName} is locked. Only project members can comment.'), + { issuableDisplayName: this.issuableDisplayName }, + ); + }, }, - mixins: [Issuable], }; </script> @@ -15,7 +27,11 @@ export default { <span class="issuable-note-warning inline"> <icon :size="16" name="lock" class="icon" /> <span> - This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment. + {{ lockedIssueWarning }} + + <gl-link :href="lockedIssueDocsPath" target="_blank" class="learn-more"> + {{ __('Learn more') }} + </gl-link> </span> </span> </div> diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue new file mode 100644 index 00000000000..228bb652597 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -0,0 +1,155 @@ +<script> +import { mapGetters } from 'vuex'; +import { SYSTEM_NOTE } from '../constants'; +import { __ } from '~/locale'; +import NoteableNote from './noteable_note.vue'; +import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import SystemNote from '~/vue_shared/components/notes/system_note.vue'; +import ToggleRepliesWidget from './toggle_replies_widget.vue'; +import NoteEditedText from './note_edited_text.vue'; + +export default { + name: 'DiscussionNotes', + components: { + ToggleRepliesWidget, + NoteEditedText, + }, + props: { + discussion: { + type: Object, + required: true, + }, + isExpanded: { + type: Boolean, + required: false, + default: false, + }, + diffLine: { + type: Object, + required: false, + default: null, + }, + line: { + type: Object, + required: false, + default: null, + }, + shouldGroupReplies: { + type: Boolean, + required: false, + default: false, + }, + helpPagePath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + ...mapGetters(['userCanReply']), + hasReplies() { + return Boolean(this.replies.length); + }, + replies() { + return this.discussion.notes.slice(1); + }, + firstNote() { + return this.discussion.notes.slice(0, 1)[0]; + }, + resolvedText() { + return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved'); + }, + commit() { + if (!this.discussion.for_commit) { + return null; + } + + return { + id: this.discussion.commit_id, + url: this.discussion.discussion_path, + }; + }, + }, + methods: { + componentName(note) { + if (note.isPlaceholderNote) { + if (note.placeholderType === SYSTEM_NOTE) { + return PlaceholderSystemNote; + } + + return PlaceholderNote; + } + + if (note.system) { + return SystemNote; + } + + return NoteableNote; + }, + componentData(note) { + return note.isPlaceholderNote ? note.notes[0] : note; + }, + }, +}; +</script> + +<template> + <div class="discussion-notes"> + <ul class="notes"> + <template v-if="shouldGroupReplies"> + <component + :is="componentName(firstNote)" + :note="componentData(firstNote)" + :line="line" + :commit="commit" + :help-page-path="helpPagePath" + :show-reply-button="userCanReply" + @handle-delete-note="$emit('deleteNote')" + @start-replying="$emit('startReplying')" + > + <note-edited-text + v-if="discussion.resolved" + slot="discussion-resolved-text" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline discussion-resolved-text" + /> + <slot slot="avatar-badge" name="avatar-badge"></slot> + </component> + <toggle-replies-widget + v-if="hasReplies" + :collapsed="!isExpanded" + :replies="replies" + @toggle="$emit('toggleDiscussion')" + /> + <template v-if="isExpanded"> + <component + :is="componentName(note)" + v-for="note in replies" + :key="note.id" + :note="componentData(note)" + :help-page-path="helpPagePath" + :line="line" + @handle-delete-note="$emit('deleteNote')" + /> + </template> + </template> + <template v-else> + <component + :is="componentName(note)" + v-for="(note, index) in discussion.notes" + :key="note.id" + :note="componentData(note)" + :help-page-path="helpPagePath" + :line="diffLine" + @handle-delete-note="$emit('deleteNote')" + > + <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> + </component> + </template> + </ul> + <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index de1ea0f58d6..844d0c3e376 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -2,6 +2,7 @@ import { mapGetters } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status'; import ReplyButton from './note_actions/reply_button.vue'; export default { @@ -14,6 +15,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [resolvedStatusMixin], props: { authorId: { type: Number, @@ -86,9 +88,6 @@ export default { }, computed: { ...mapGetters(['getUserDataByProp']), - showReplyButton() { - return gon.features && gon.features.replyToIndividualNotes && this.showReply; - }, shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, @@ -101,15 +100,6 @@ export default { currentUserId() { return this.getUserDataByProp('id'); }, - resolveButtonTitle() { - let title = 'Mark as resolved'; - - if (this.resolvedBy) { - title = `Resolved by ${this.resolvedBy.name}`; - } - - return title; - }, }, methods: { onEdit() { @@ -145,7 +135,7 @@ export default { @click="onResolve" > <template v-if="!isResolving"> - <icon name="check-circle" /> + <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" /> </template> <gl-loading-icon v-else inline /> </button> @@ -157,18 +147,15 @@ export default { class="note-action-button note-emoji-button js-add-award js-note-emoji" href="#" title="Add reaction" + data-position="right" > - <gl-loading-icon inline /> - <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" /> + <icon css-classes="link-highlight award-control-icon-neutral" name="slight-smile" /> + <icon css-classes="link-highlight award-control-icon-positive" name="smiley" /> + <icon css-classes="link-highlight award-control-icon-super-positive" name="smiley" /> </a> </div> <reply-button - v-if="showReplyButton" + v-if="showReply" ref="replyButton" class="js-reply-button" @startReplying="$emit('startReplying')" @@ -208,7 +195,7 @@ export default { </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> - <a :href="reportAbusePath">{{ __('Report abuse to GitLab') }}</a> + <a :href="reportAbusePath">{{ __('Report abuse to admin') }}</a> </li> <li v-if="noteUrl"> <button diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index f50cab81efe..be8e42af9ea 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -18,7 +18,7 @@ export default { <div class="note-actions-item"> <gl-button ref="button" - v-gl-tooltip.bottom + v-gl-tooltip class="note-action-button" variant="transparent" :title="__('Reply to comment')" diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 17e5fcab5b7..941b6d5cab3 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -189,13 +189,13 @@ export default { type="button" > <span class="award-control-icon award-control-icon-neutral"> - <icon name="emoji_slightly_smiling_face" /> + <icon name="slight-smile" /> </span> <span class="award-control-icon award-control-icon-positive"> - <icon name="emoji_smiley" /> + <icon name="smiley" /> </span> <span class="award-control-icon award-control-icon-super-positive"> - <icon name="emoji_smiley" /> + <icon name="smiley" /> </span> <i aria-hidden="true" diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index fb1d98355b3..88454c3fb4c 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,6 +1,7 @@ <script> import { mapActions } from 'vuex'; import $ from 'jquery'; +import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion'; import noteEditedText from './note_edited_text.vue'; import noteAwardsList from './note_awards_list.vue'; import noteAttachment from './note_attachment.vue'; @@ -16,7 +17,7 @@ export default { noteForm, Suggestions, }, - mixins: [autosave], + mixins: [autosave, getDiscussion], props: { note: { type: Object, @@ -76,16 +77,18 @@ export default { renderGFM() { $(this.$refs['note-body']).renderGFM(); }, - handleFormUpdate(note, parentElement, callback) { - this.$emit('handleFormUpdate', note, parentElement, callback); + handleFormUpdate(note, parentElement, callback, resolveDiscussion) { + this.$emit('handleFormUpdate', note, parentElement, callback, resolveDiscussion); }, formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelForm', shouldConfirm, isDirty); }, - applySuggestion({ suggestionId, flashContainer, callback }) { + applySuggestion({ suggestionId, flashContainer, callback = () => {} }) { const { discussion_id: discussionId, id: noteId } = this.note; - this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer, callback }); + return this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer }).then( + callback, + ); }, }, }; @@ -95,7 +98,6 @@ export default { <div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body"> <suggestions v-if="hasSuggestion && !isEditing" - class="note-text md" :suggestions="note.suggestions" :note-html="note.note_html" :line-type="lineType" @@ -112,6 +114,8 @@ export default { :line="line" :note="note" :help-page-path="helpPagePath" + :discussion="discussion" + :resolve-discussion="note.resolve_discussion" @handleFormUpdate="handleFormUpdate" @cancelForm="formCancelHandler" /> @@ -120,6 +124,7 @@ export default { v-model="note.note" :data-update-url="note.path" class="hidden js-task-list-field" + dir="auto" ></textarea> <note-edited-text v-if="note.last_edited_at" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 92258a25438..09ecb695214 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -7,6 +7,8 @@ import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; import { __ } from '~/locale'; +import { getDraft, updateDraft } from '~/lib/utils/autosave'; +import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; export default { name: 'NoteForm', @@ -14,7 +16,7 @@ export default { issueWarning, markdownField, }, - mixins: [issuableStateMixin, resolvable], + mixins: [issuableStateMixin, resolvable, noteFormMixin], props: { noteBody: { type: String, @@ -60,15 +62,31 @@ export default { required: false, default: null, }, + diffFile: { + type: Object, + required: false, + default: null, + }, helpPagePath: { type: String, required: false, default: '', }, + autosaveKey: { + type: String, + required: false, + default: '', + }, }, data() { + let updatedNoteBody = this.noteBody; + + if (!updatedNoteBody && this.autosaveKey) { + updatedNoteBody = getDraft(this.autosaveKey) || ''; + } + return { - updatedNoteBody: this.noteBody, + updatedNoteBody, conflictWhileEditing: false, isSubmitting: false, isResolving: this.resolveDiscussion, @@ -90,9 +108,42 @@ export default { } return '#'; }, + diffParams() { + if (this.diffFile) { + return { + filePath: this.diffFile.file_path, + refs: this.diffFile.diff_refs, + }; + } else if (this.note && this.note.position) { + return { + filePath: this.note.position.new_path, + refs: this.note.position, + }; + } else if (this.discussion && this.discussion.diff_file) { + return { + filePath: this.discussion.diff_file.file_path, + refs: this.discussion.diff_file.diff_refs, + }; + } + + return null; + }, markdownPreviewPath() { const notable = this.getNoteableDataByProp('preview_note_path'); - return mergeUrlParams({ preview_suggestions: true }, notable); + + const previewSuggestions = this.line && this.diffParams; + const params = previewSuggestions + ? { + preview_suggestions: previewSuggestions, + line: this.line.new_line, + file_path: this.diffParams.filePath, + base_sha: this.diffParams.refs.base_sha, + start_sha: this.diffParams.refs.start_sha, + head_sha: this.diffParams.refs.head_sha, + } + : {}; + + return mergeUrlParams(params, notable); }, markdownDocsPath() { return this.getNotesDataByProp('markdownDocsPath'); @@ -145,21 +196,6 @@ export default { return shouldResolve || shouldToggleState; }, - handleKeySubmit() { - this.handleUpdate(); - }, - handleUpdate(shouldResolve) { - const beforeSubmitDiscussionState = this.discussionResolved; - this.isSubmitting = true; - - this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { - this.isSubmitting = false; - - if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) { - this.resolveHandler(beforeSubmitDiscussionState); - } - }); - }, editMyLastNote() { if (this.updatedNoteBody === '') { const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion); @@ -175,6 +211,12 @@ export default { // Sends information about confirm message and if the textarea has changed this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); }, + onInput() { + if (this.autosaveKey) { + const { autosaveKey, updatedNoteBody: text } = this; + updateDraft(autosaveKey, text); + } + }, }, }; </script> @@ -192,6 +234,8 @@ export default { v-if="hasWarning(getNoteableData)" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" + :locked-issue-docs-path="lockedIssueDocsPath" + :confidential-issue-docs-path="confidentialIssueDocsPath" /> <markdown-field @@ -212,37 +256,85 @@ export default { :data-supports-quick-actions="!isEditing" name="note[note]" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" + dir="auto" aria-label="Description" placeholder="Write a comment or drag your files hereā¦" @keydown.meta.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()" - @keydown.up="editMyLastNote()" - @keydown.esc="cancelHandler(true)" + @keydown.exact.up="editMyLastNote()" + @keydown.exact.esc="cancelHandler(true)" + @input="onInput" ></textarea> </markdown-field> <div class="note-form-actions clearfix"> - <button - :disabled="isDisabled" - type="button" - class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" - @click="handleUpdate()" - > - {{ saveButtonTitle }} - </button> - <button - v-if="discussion.resolvable" - class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" - @click.prevent="handleUpdate(true)" - > - {{ resolveButtonTitle }} - </button> - <button - class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" - type="button" - @click="cancelHandler()" - > - Cancel - </button> + <template v-if="showBatchCommentsActions"> + <p v-if="showResolveDiscussionToggle"> + <label> + <template v-if="discussionResolved"> + <input + v-model="isUnresolving" + type="checkbox" + class="qa-unresolve-review-discussion" + /> + {{ __('Unresolve discussion') }} + </template> + <template v-else> + <input v-model="isResolving" type="checkbox" class="qa-resolve-review-discussion" /> + {{ __('Resolve discussion') }} + </template> + </label> + </p> + <div> + <button + :disabled="isDisabled" + type="button" + class="btn btn-success qa-start-review" + @click="handleAddToReview" + > + <template v-if="hasDrafts">{{ __('Add to review') }}</template> + <template v-else>{{ __('Start a review') }}</template> + </button> + <button + :disabled="isDisabled" + type="button" + class="btn qa-comment-now" + @click="handleUpdate()" + > + {{ __('Add comment now') }} + </button> + <button + class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" + type="button" + @click="cancelHandler()" + > + {{ __('Cancel') }} + </button> + </div> + </template> + <template v-else> + <button + :disabled="isDisabled" + type="button" + class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" + @click="handleUpdate()" + > + {{ saveButtonTitle }} + </button> + <button + v-if="discussion.resolvable" + class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + @click.prevent="handleUpdate(true)" + > + {{ resolveButtonTitle }} + </button> + <button + class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" + type="button" + @click="cancelHandler()" + > + Cancel + </button> + </template> </div> </form> </div> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 7b39901024d..fbf82fab9e9 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -69,7 +69,7 @@ export default { type="button" @click="handleToggle" > - <i :class="toggleChevronClass" class="fa" aria-hidden="true"> </i> + <i :class="toggleChevronClass" class="fa" aria-hidden="true"></i> {{ __('Toggle discussion') }} </button> </div> @@ -81,35 +81,31 @@ export default { :data-user-id="author.id" :data-username="author.username" > - <span class="note-header-author-name">{{ author.name }}</span> + <slot name="note-header-info"></slot> + <span class="note-header-author-name bold">{{ author.name }}</span> <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> - <span class="note-headline-light"> @{{ author.username }} </span> + <span class="note-headline-light">@{{ author.username }}</span> </a> - <span v-else> {{ __('A deleted user') }} </span> - <span class="note-headline-light"> - <span class="note-headline-meta"> - <span class="system-note-message"> <slot></slot> </span> - <template v-if="createdAt"> - <span class="system-note-separator"> - <template v-if="actionText"> - {{ actionText }} - </template> - </span> - <a - :href="noteTimestampLink" - class="note-timestamp system-note-separator" - @click="updateTargetNoteHash" - > - <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> - </a> - </template> - <i - class="fa fa-spinner fa-spin editing-spinner" - aria-label="Comment is being updated" - aria-hidden="true" + <span v-else>{{ __('A deleted user') }}</span> + <span class="note-headline-light note-headline-meta"> + <span class="system-note-message"> <slot></slot> </span> + <template v-if="createdAt"> + <span class="system-note-separator"> + <template v-if="actionText">{{ actionText }}</template> + </span> + <a + :href="noteTimestampLink" + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash" > - </i> - </span> + <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> + </a> + </template> + <i + class="fa fa-spinner fa-spin editing-spinner" + aria-label="Comment is being updated" + aria-hidden="true" + ></i> </span> </div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 3894dc8c677..eb6a4a67fff 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -4,55 +4,42 @@ import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, __, sprintf } from '~/locale'; -import systemNote from '~/vue_shared/components/notes/system_note.vue'; +import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; import icon from '~/vue_shared/components/icon.vue'; +import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.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 resolveDiscussionButton from './discussion_resolve_button.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'; import diffWithNote from './diff_with_note.vue'; -import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; -import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; -import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; -import ReplyPlaceholder from './discussion_reply_placeholder.vue'; -import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue'; -import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; import eventHub from '../event_hub'; +import DiscussionNotes from './discussion_notes.vue'; +import DiscussionActions from './discussion_actions.vue'; export default { name: 'NoteableDiscussion', components: { icon, - noteableNote, userAvatarLink, noteHeader, noteSignedOutWidget, noteEditedText, noteForm, - resolveDiscussionButton, - jumpToNextDiscussionButton, - toggleRepliesWidget, - ReplyPlaceholder, - placeholderNote, - placeholderSystemNote, - ResolveWithIssueButton, - systemNote, + DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'), TimelineEntryItem, + DiscussionNotes, + DiscussionActions, }, directives: { GlTooltip: GlTooltipDirective, }, - mixins: [autosave, noteable, resolvable, discussionNavigation], + mixins: [noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], props: { discussion: { type: Object, @@ -85,42 +72,38 @@ export default { }, }, data() { - const { diff_discussion: isDiffDiscussion, resolved } = this.discussion; - return { isReplying: false, isResolving: false, resolveAsThread: true, - isRepliesCollapsed: Boolean(!isDiffDiscussion && resolved), }; }, computed: { ...mapGetters([ 'convertedDisscussionIds', 'getNoteableData', + 'userCanReply', 'nextUnresolvedDiscussionId', 'unresolvedDiscussionsCount', 'hasUnresolvedDiscussions', 'showJumpToNextDiscussion', + 'getUserData', ]), + currentUser() { + return this.getUserData; + }, author() { - return this.initialDiscussion.author; + return this.firstNote.author; }, - canReply() { - return this.getNoteableData.current_user.can_create_note; + autosaveKey() { + return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id); }, newNotePath() { return this.getNoteableData.create_note_path; }, - hasReplies() { - return this.discussion.notes.length > 1; - }, - initialDiscussion() { + firstNote() { return this.discussion.notes.slice(0, 1)[0]; }, - replies() { - return this.discussion.notes.slice(1); - }, lastUpdatedBy() { const { notes } = this.discussion; @@ -173,11 +156,11 @@ export default { return ''; }, - shouldShowDiscussions() { - const { expanded, resolved } = this.discussion; - const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved; - - return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion; + isExpanded() { + return this.discussion.expanded || this.alwaysExpanded; + }, + shouldHideDiscussionBody() { + return this.shouldRenderDiffs && !this.isExpanded; }, actionText() { const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; @@ -226,30 +209,8 @@ export default { return null; }, - commit() { - if (!this.discussion.for_commit) { - return null; - } - - return { - id: this.discussion.commit_id, - url: this.discussion.discussion_path, - }; - }, resolveWithIssuePath() { - return !this.discussionResolved && this.discussion.resolve_with_issue_path; - }, - }, - watch: { - isReplying() { - if (this.isReplying) { - this.$nextTick(() => { - // Pass an extra key to separate reply and note edit forms - this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']); - }); - } else { - this.disposeAutoSave(); - } + return !this.discussionResolved ? this.discussion.resolve_with_issue_path : ''; }, }, created() { @@ -268,30 +229,9 @@ export default { 'removeConvertedDiscussion', ]), truncateSha, - componentName(note) { - if (note.isPlaceholderNote) { - if (note.placeholderType === SYSTEM_NOTE) { - return placeholderSystemNote; - } - - return placeholderNote; - } - - if (note.system) { - return systemNote; - } - - return noteableNote; - }, - componentData(note) { - return note.isPlaceholderNote ? note.notes[0] : note; - }, toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); }, - toggleReplies() { - this.isRepliesCollapsed = !this.isRepliesCollapsed; - }, showReplyForm() { this.isReplying = true; }, @@ -310,7 +250,7 @@ export default { } this.isReplying = false; - this.resetAutoSave(); + clearDraft(this.autosaveKey); }, saveReply(noteText, form, callback) { const postData = { @@ -336,7 +276,7 @@ export default { this.isReplying = false; this.saveNote(replyData) .then(() => { - this.resetAutoSave(); + clearDraft(this.autosaveKey); callback(); }) .catch(err => { @@ -388,8 +328,8 @@ Please check your network connection and try again.`; <div class="timeline-content"> <note-header :author="author" - :created-at="initialDiscussion.created_at" - :note-id="initialDiscussion.id" + :created-at="firstNote.created_at" + :note-id="firstNote.id" :include-toggle="true" :expanded="discussion.expanded" @toggleHandler="toggleDiscussionHandler" @@ -412,110 +352,79 @@ Please check your network connection and try again.`; /> </div> </div> - <div v-if="shouldShowDiscussions" class="discussion-body"> + <div v-if="!shouldHideDiscussionBody" class="discussion-body"> <component :is="wrapperComponent" v-bind="wrapperComponentProps" class="card discussion-wrapper" > - <div class="discussion-notes"> - <ul class="notes"> - <template v-if="shouldGroupReplies"> - <component - :is="componentName(initialDiscussion)" - :note="componentData(initialDiscussion)" - :line="line" - :commit="commit" - :help-page-path="helpPagePath" - :show-reply-button="canReply" - @handleDeleteNote="deleteNoteHandler" - @startReplying="showReplyForm" - > - <note-edited-text - v-if="discussion.resolved" - slot="discussion-resolved-text" - :edited-at="discussion.resolved_at" - :edited-by="discussion.resolved_by" - :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline discussion-resolved-text" - /> - <slot slot="avatar-badge" name="avatar-badge"></slot> - </component> - <toggle-replies-widget - v-if="hasReplies" - :collapsed="isRepliesCollapsed" - :replies="replies" - @toggle="toggleReplies" + <discussion-notes + :discussion="discussion" + :diff-line="diffLine" + :help-page-path="helpPagePath" + :is-expanded="isExpanded" + :line="line" + :should-group-replies="shouldGroupReplies" + @startReplying="showReplyForm" + @toggleDiscussion="toggleDiscussionHandler" + @deleteNote="deleteNoteHandler" + > + <slot slot="avatar-badge" name="avatar-badge"></slot> + <template #footer="{ showReplies }"> + <draft-note + v-if="showDraft(discussion.reply_id)" + :key="`draft_${discussion.id}`" + :draft="draftForDiscussion(discussion.reply_id)" + /> + <div + v-else-if="showReplies" + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder" + > + <user-avatar-link + v-if="!isReplying && currentUser" + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :img-alt="currentUser.name" + :img-size="40" + class="d-none d-sm-block" + /> + <discussion-actions + v-if="!isReplying && userCanReply" + :discussion="discussion" + :is-resolving="isResolving" + :resolve-button-title="resolveButtonTitle" + :resolve-with-issue-path="resolveWithIssuePath" + :should-show-jump-to-next-discussion="shouldShowJumpToNextDiscussion" + @showReplyForm="showReplyForm" + @resolve="resolveHandler" + @jumpToNextDiscussion="jumpToNextDiscussion" /> - <template v-if="!isRepliesCollapsed"> - <component - :is="componentName(note)" - v-for="note in replies" - :key="note.id" - :note="componentData(note)" - :help-page-path="helpPagePath" - :line="line" - @handleDeleteNote="deleteNoteHandler" + <div v-if="isReplying" class="avatar-note-form-holder"> + <user-avatar-link + v-if="currentUser" + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :img-alt="currentUser.name" + :img-size="40" + class="d-none d-sm-block" /> - </template> - </template> - <template v-else> - <component - :is="componentName(note)" - v-for="(note, index) in discussion.notes" - :key="note.id" - :note="componentData(note)" - :help-page-path="helpPagePath" - :line="diffLine" - @handleDeleteNote="deleteNoteHandler" - > - <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> - </component> - </template> - </ul> - <div - v-if="!isRepliesCollapsed || !hasReplies" - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder" - > - <template v-if="!isReplying && canReply"> - <div class="discussion-with-resolve-btn"> - <reply-placeholder class="qa-discussion-reply" @onClick="showReplyForm" /> - <resolve-discussion-button - v-if="discussion.resolvable" - :is-resolving="isResolving" - :button-title="resolveButtonTitle" - @onClick="resolveHandler" + <note-form + ref="noteForm" + :discussion="discussion" + :is-editing="false" + :line="diffLine" + save-button-title="Comment" + :autosave-key="autosaveKey" + @handleFormUpdateAddToReview="addReplyToReview" + @handleFormUpdate="saveReply" + @cancelForm="cancelReplyForm" /> - <div - v-if="discussion.resolvable" - class="btn-group discussion-actions ml-sm-2" - role="group" - > - <resolve-with-issue-button - v-if="resolveWithIssuePath" - :url="resolveWithIssuePath" - /> - <jump-to-next-discussion-button - v-if="shouldShowJumpToNextDiscussion" - @onClick="jumpToNextDiscussion" - /> - </div> </div> - </template> - <note-form - v-if="isReplying" - ref="noteForm" - :discussion="discussion" - :is-editing="false" - :line="diffLine" - save-button-title="Comment" - @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" - /> - <note-signed-out-widget v-if="!canReply" /> - </div> - </div> + <note-signed-out-widget v-if="!userCanReply" /> + </div> + </template> + </discussion-notes> </component> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 04e74a43acc..aa80e25a3e0 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -4,12 +4,13 @@ import { mapGetters, mapActions } from 'vuex'; import { escape } from 'underscore'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import draftMixin from 'ee_else_ce/notes/mixins/draft'; import { s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; import noteActions from './note_actions.vue'; -import noteBody from './note_body.vue'; +import NoteBody from './note_body.vue'; import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; @@ -20,10 +21,10 @@ export default { userAvatarLink, noteHeader, noteActions, - noteBody, + NoteBody, TimelineEntryItem, }, - mixins: [noteable, resolvable], + mixins: [noteable, resolvable, draftMixin], props: { note: { type: Object, @@ -73,11 +74,8 @@ export default { 'is-editable': this.note.current_user.can_edit, }; }, - canResolve() { - return this.note.resolvable && !!this.getUserData.id; - }, canReportAsAbuse() { - return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id; + return Boolean(this.note.report_abuse_path) && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; @@ -96,7 +94,7 @@ export default { return ''; } - // We need to do this to ensure we have the currect sentence order + // We need to do this to ensure we have the correct sentence order // when translating this as the sentence order may change from one // language to the next. See: // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771 @@ -156,12 +154,16 @@ export default { this.$refs.noteBody.resetAutoSave(); this.$emit('updateSuccess'); }, - formUpdateHandler(noteText, parentElement, callback) { + formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) { this.$emit('handleUpdateNote', { note: this.note, noteText, + resolveDiscussion, callback: () => this.updateSuccess(), }); + + if (this.isDraft) return; + const data = { endpoint: this.note.path, note: { @@ -207,7 +209,10 @@ export default { // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content this.note.note = noteText; - this.$refs.noteBody.note.note = noteText; + const { noteBody } = this.$refs; + if (noteBody) { + noteBody.note.note = noteText; + } }, }, }; @@ -219,7 +224,7 @@ export default { :class="classNameBindings" :data-award-url="note.toggle_award_path" :data-note-id="note.id" - class="note note-wrapper" + class="note note-wrapper qa-noteable-note-item" > <div v-once class="timeline-icon"> <user-avatar-link @@ -234,6 +239,7 @@ export default { <div class="timeline-content"> <div class="note-header"> <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> + <slot slot="note-header-info" name="note-header-info"></slot> <span v-if="commit" v-html="actionText"></span> <span v-else class="d-none d-sm-inline">·</span> </note-header> @@ -247,12 +253,15 @@ export default { :can-award-emoji="note.current_user.can_award_emoji" :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" - :can-resolve="note.current_user.can_resolve" + :can-resolve="canResolve" :report-abuse-path="note.report_abuse_path" - :resolvable="note.resolvable" - :is-resolved="note.resolved" + :resolvable="note.resolvable || note.isDraft" + :is-resolved="note.resolved || note.resolve_discussion" :is-resolving="isResolving" :resolved-by="note.resolved_by" + :is-draft="note.isDraft" + :resolve-discussion="note.isDraft && note.resolve_discussion" + :discussion-id="discussionId" @handleEdit="editHandler" @handleDelete="deleteHandler" @handleResolve="resolveHandler" diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 8d3f6d902f8..4d00e957973 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -6,6 +6,7 @@ import * as constants from '../constants'; import eventHub from '../event_hub'; import noteableNote from './noteable_note.vue'; import noteableDiscussion from './noteable_discussion.vue'; +import discussionFilterNote from './discussion_filter_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue'; import commentForm from './comment_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; @@ -24,6 +25,7 @@ export default { placeholderNote, placeholderSystemNote, skeletonLoadingContainer, + discussionFilterNote, }, props: { noteableData: { @@ -65,6 +67,7 @@ export default { 'isLoading', 'commentsDisabled', 'getNoteableData', + 'userCanReply', ]), noteableType() { return this.noteableData.noteableType; @@ -81,7 +84,7 @@ export default { return this.discussions; }, canReply() { - return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled; + return this.userCanReply && !this.commentsDisabled; }, }, watch: { @@ -124,6 +127,9 @@ export default { initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, + beforeDestroy() { + this.stopPolling(); + }, methods: { ...mapActions([ 'setLoadingState', @@ -141,6 +147,7 @@ export default { 'expandDiscussion', 'startTaskList', 'convertToDiscussion', + 'stopPolling', ]), fetchNotes() { if (this.isFetching) return null; @@ -235,6 +242,7 @@ export default { :help-page-path="helpPagePath" /> </template> + <discussion-filter-note v-show="commentsDisabled" /> </ul> <comment-form v-if="!commentsDisabled" :noteable-type="noteableType" /> |