diff options
Diffstat (limited to 'app/assets/javascripts/notes/components')
14 files changed, 503 insertions, 309 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 72d7e22fba0..c6a524f68cb 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -7,10 +7,7 @@ import { __, sprintf } from '~/locale'; import Flash from '../../flash'; import Autosave from '../../autosave'; import TaskList from '../../task_list'; -import { - capitalizeFirstCharacter, - convertToCamelCase, -} from '../../lib/utils/text_utility'; +import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; @@ -56,21 +53,23 @@ export default { ]), ...mapState(['isToggleStateButtonLoading']), noteableDisplayName() { - return this.noteableType.replace(/_/g, ' '); + return splitCamelCase(this.noteableType).toLowerCase(); }, isLoggedIn() { return this.getUserData.id; }, commentButtonTitle() { - return this.noteType === constants.COMMENT - ? 'Comment' - : 'Start discussion'; + return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; + }, + startDiscussionDescription() { + let text = 'Discuss a specific suggestion or question'; + if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) { + text += ' that needs to be resolved'; + } + return `${text}.`; }, isOpen() { - return ( - this.openState === constants.OPENED || - this.openState === constants.REOPENED - ); + return this.openState === constants.OPENED || this.openState === constants.REOPENED; }, canCreateNote() { return this.getNoteableData.current_user.can_create_note; @@ -117,6 +116,9 @@ export default { endpoint() { return this.getNoteableData.create_note_path; }, + issuableTypeTitle() { + return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue'; + }, }, watch: { note(newNote) { @@ -129,9 +131,7 @@ export default { mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. $(document).on('issuable:change', (e, isClosed) => { - this.toggleIssueLocalState( - isClosed ? constants.CLOSED : constants.REOPENED, - ); + this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED); }); this.initAutoSave(); @@ -168,6 +168,7 @@ export default { noteable_id: this.getNoteableData.id, note: this.note, }, + merge_request_diff_head_sha: this.getNoteableData.diff_head_sha, }, }; @@ -227,9 +228,7 @@ Please check your network connection and try again.`; this.toggleStateButtonLoading(false); Flash( sprintf( - __( - 'Something went wrong while closing the %{issuable}. Please try again later', - ), + __('Something went wrong while closing the %{issuable}. Please try again later'), { issuable: this.noteableDisplayName }, ), ); @@ -242,9 +241,7 @@ Please check your network connection and try again.`; this.toggleStateButtonLoading(false); Flash( sprintf( - __( - 'Something went wrong while reopening the %{issuable}. Please try again later', - ), + __('Something went wrong while reopening the %{issuable}. Please try again later'), { issuable: this.noteableDisplayName }, ), ); @@ -281,9 +278,7 @@ Please check your network connection and try again.`; }, initAutoSave() { if (this.isLoggedIn) { - const noteableType = capitalizeFirstCharacter( - convertToCamelCase(this.noteableType), - ); + const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); this.autosave = new Autosave($(this.$refs.textarea), [ 'Note', @@ -312,8 +307,8 @@ Please check your network connection and try again.`; <div> <note-signed-out-widget v-if="!isLoggedIn" /> <discussion-locked-widget - issuable-type="issue" - v-else-if="isLocked(getNoteableData) && !canCreateNote" + v-else-if="!canCreateNote" + :issuable-type="issuableTypeTitle" /> <ul v-else-if="canCreateNote" @@ -345,23 +340,23 @@ Please check your network connection and try again.`; /> <markdown-field + ref="markdownField" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" - :add-spacing-classes="false" - ref="markdownField"> + :add-spacing-classes="false"> <textarea id="note-body" + ref="textarea" + slot="textarea" + v-model="note" + :disabled="isSubmitting" name="note[note]" - class="note-textarea js-vue-comment-form + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" - v-model="note" - ref="textarea" - slot="textarea" - :disabled="isSubmitting" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files hereā¦" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()"> @@ -372,10 +367,10 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" class="float-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button - @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" - type="submit"> + type="submit" + @click.prevent="handleSave()"> {{ __(commentButtonTitle) }} </button> <button @@ -423,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <div class="description"> <strong>Start discussion</strong> <p> - Discuss a specific suggestion or question. + {{ startDiscussionDescription }} </p> </div> </button> @@ -434,20 +429,20 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <loading-button v-if="canUpdateIssue" :loading="isToggleStateButtonLoading" - @click="handleSave(true)" :container-class="[ actionButtonClassNames, 'btn btn-comment btn-comment-and-close js-action-button' ]" :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" + @click="handleSave(true)" /> <button - type="button" v-if="note.length" - @click="discard" - class="btn btn-cancel js-note-discard"> + type="button" + class="btn btn-cancel js-note-discard" + @click="discard"> Discard draft </button> </div> diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue index 94d9dc69964..fc7b52be241 100644 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -29,12 +29,12 @@ export default { <span> <icon name="archive" /> <strong - v-html="diffFile.submoduleLink" class="file-title-name" + v-html="diffFile.submoduleLink" ></strong> <clipboard-button - title="Copy file path to clipboard" :text="diffFile.submoduleLink" + title="Copy file path to clipboard" css-class="btn-default btn-transparent btn-clipboard" /> </span> @@ -48,16 +48,16 @@ export default { <span v-html="diffFile.blobIcon"></span> <span v-if="diffFile.renamedFile"> <strong - class="file-title-name has-tooltip" :title="diffFile.oldPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.oldPath }} </strong> → <strong - class="file-title-name has-tooltip" :title="diffFile.newPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.newPath }} @@ -66,8 +66,8 @@ export default { <strong v-else - class="file-title-name has-tooltip" :title="diffFile.oldPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.filePath }} @@ -78,8 +78,8 @@ export default { </component> <clipboard-button - title="Copy file path to clipboard" :text="diffFile.filePath" + title="Copy file path to clipboard" css-class="btn-default btn-transparent btn-clipboard" /> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index ee01ec85bbb..d321f2ce15e 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,13 +1,15 @@ <script> -import $ from 'jquery'; -import syntaxHighlight from '~/syntax_highlight'; +import { mapState, mapActions } from 'vuex'; import imageDiffHelper from '~/image_diff/helpers/index'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import DiffFileHeader from './diff_file_header.vue'; +import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; export default { components: { DiffFileHeader, + SkeletonLoadingContainer, }, props: { discussion: { @@ -15,7 +17,24 @@ export default { required: true, }, }, + data() { + return { + error: false, + }; + }, computed: { + ...mapState({ + noteableData: state => state.notes.noteableData, + }), + hasTruncatedDiffLines() { + return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0; + }, + isDiscussionsExpanded() { + return true; // TODO: @fatihacet - Fix this. + }, + isCollapsed() { + return this.diffFile.collapsed || false; + }, isImageDiff() { return !this.diffFile.text; }, @@ -23,36 +42,46 @@ export default { const { text } = this.diffFile; return text ? 'text-file' : 'js-image-file'; }, - diffRows() { - return $(this.discussion.truncatedDiffLines); - }, diffFile() { - return convertObjectPropsToCamelCase(this.discussion.diffFile); + return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true }); }, imageDiffHtml() { return this.discussion.imageDiffHtml; }, + currentUser() { + return this.noteableData.current_user; + }, + userColorScheme() { + return window.gon.user_color_scheme; + }, + normalizedDiffLines() { + const lines = this.discussion.truncatedDiffLines || []; + + return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line))); + }, }, mounted() { if (this.isImageDiff) { const canCreateNote = false; const renderCommentBadge = true; - imageDiffHelper.initImageDiff( - this.$refs.fileHolder, - canCreateNote, - renderCommentBadge, - ); - } else { - const fileHolder = $(this.$refs.fileHolder); - this.$nextTick(() => { - syntaxHighlight(fileHolder); - }); + imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); + } else if (!this.hasTruncatedDiffLines) { + this.fetchDiff(); } }, methods: { + ...mapActions(['fetchDiscussionDiffLines']), rowTag(html) { return html.outerHTML ? 'tr' : 'template'; }, + fetchDiff() { + this.error = false; + this.fetchDiscussionDiffLines(this.discussion) + .then(this.highlight) + .catch(() => { + this.error = true; + }); + }, }, }; </script> @@ -60,26 +89,62 @@ export default { <template> <div ref="fileHolder" - class="diff-file file-holder" :class="diffFileClass" + class="diff-file file-holder" > - <div class="js-file-title file-title file-title-flex-parent"> - <diff-file-header - :diff-file="diffFile" - /> - </div> + <diff-file-header + :diff-file="diffFile" + :current-user="currentUser" + :discussions-expanded="isDiscussionsExpanded" + :expanded="!isCollapsed" + /> <div v-if="diffFile.text" - class="diff-content code js-syntax-highlight" + :class="userColorScheme" + class="diff-content code" > <table> - <component - :is="rowTag(html)" - :class="html.className" - v-for="(html, index) in diffRows" - v-html="html.outerHTML" - :key="index" - /> + <tr + v-for="line in normalizedDiffLines" + :key="line.lineCode" + class="line_holder" + > + <td class="diff-line-num old_line">{{ line.oldLine }}</td> + <td class="diff-line-num new_line">{{ line.newLine }}</td> + <td + :class="line.type" + class="line_content" + v-html="line.richText" + > + </td> + </tr> + <tr + v-if="!hasTruncatedDiffLines" + class="line_holder line-holder-placeholder" + > + <td class="old_line diff-line-num"></td> + <td class="new_line diff-line-num"></td> + <td + v-if="error" + class="js-error-lazy-load-diff diff-loading-error-block" + > + Unable to load the diff + <button + class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button" + @click="fetchDiff" + > + Try again + </button> + </td> + <td + v-else + class="line_content js-success-lazy-load" + > + <span></span> + <skeleton-loading-container /> + <span></span> + </td> + </tr> <tr class="notes_holder"> <td class="notes_line" diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index cbe4774a360..6385b75e557 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters } from 'vuex'; +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'; @@ -48,10 +48,14 @@ export default { this.nextDiscussionSvg = nextDiscussionSvg; }, methods: { - jumpToFirstDiscussion() { - const el = document.querySelector( - `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`, - ); + ...mapActions(['expandDiscussion']), + jumpToFirstUnresolvedDiscussion() { + const discussionId = this.firstUnresolvedDiscussionId; + if (!discussionId) { + return; + } + + const el = document.querySelector(`[data-discussion-id="${discussionId}"]`); const activeTab = window.mrTabs.currentAction; if (activeTab === 'commits' || activeTab === 'pipelines') { @@ -59,6 +63,7 @@ export default { } if (el) { + this.expandDiscussion({ discussionId }); scrollToElement(el); } }, @@ -95,9 +100,9 @@ export default { class="btn-group" role="group"> <a - :href="resolveAllDiscussionsIssuePath" v-tooltip - title="Resolve all discussions in new issue" + :href="resolveAllDiscussionsIssuePath" + :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> @@ -108,11 +113,11 @@ export default { class="btn-group" role="group"> <button - @click="jumpToFirstDiscussion" v-tooltip title="Jump to first unresolved discussion" data-container="body" - class="btn btn-default discussion-next-btn"> + class="btn btn-default discussion-next-btn" + @click="jumpToFirstUnresolvedDiscussion"> <span v-html="nextDiscussionSvg"></span> </button> </div> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index 13283b187d1..de0a5f8489b 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -14,8 +14,8 @@ export default { <div class="disabled-comment text-center"> <span class="issuable-note-warning inline"> <icon - name="lock" :size="16" + name="lock" class="icon" /> <span> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 626b0799581..cdbbb342331 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -27,6 +27,10 @@ export default { type: Number, required: true, }, + noteUrl: { + type: String, + required: true, + }, accessLevel: { type: String, required: false, @@ -48,6 +52,11 @@ export default { type: Boolean, required: true, }, + canResolve: { + type: Boolean, + required: false, + default: false, + }, resolvable: { type: Boolean, required: false, @@ -125,16 +134,16 @@ export default { {{ accessLevel }} </span> <div - v-if="resolvable" + v-if="canResolve" class="note-actions-item"> <button v-tooltip - @click="onResolve" :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" :title="resolveButtonTitle" :aria-label="resolveButtonTitle" type="button" - class="line-resolve-btn note-action-button"> + class="line-resolve-btn note-action-button" + @click="onResolve"> <template v-if="!isResolving"> <div v-if="isResolved" @@ -164,16 +173,16 @@ export default { > <loading-icon :inline="true" /> <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> + class="link-highlight award-control-icon-neutral" + v-html="emojiSmiling"> </span> <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> + class="link-highlight award-control-icon-positive" + v-html="emojiSmiley"> </span> <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> + class="link-highlight award-control-icon-super-positive" + v-html="emojiSmile"> </span> </a> </div> @@ -181,16 +190,16 @@ export default { v-if="canEdit" class="note-actions-item"> <button - @click="onEdit" v-tooltip type="button" title="Edit comment" class="note-action-button js-note-edit btn btn-transparent" data-container="body" - data-placement="bottom"> + data-placement="bottom" + @click="onEdit"> <span - v-html="editSvg" - class="link-highlight"> + class="link-highlight" + v-html="editSvg"> </span> </button> </div> @@ -216,11 +225,20 @@ export default { Report as abuse </a> </li> + <li> + <button + :data-clipboard-text="noteUrl" + type="button" + css-class="btn-default btn-transparent" + > + Copy link + </button> + </li> <li v-if="canEdit"> <button - @click.prevent="onDelete" class="btn btn-transparent js-note-delete js-note-delete" - type="button"> + type="button" + @click.prevent="onDelete"> <span class="text-danger"> Delete comment </span> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e8fd155a1ee..225d9f18612 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -199,10 +199,11 @@ export default { :key="index" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" - @click="handleAward(awardName)" class="btn award-control" + data-boundary="viewport" data-placement="bottom" - type="button"> + type="button" + @click="handleAward(awardName)"> <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter"> {{ awardList.length }} @@ -217,19 +218,20 @@ export default { class="award-control btn js-add-award" title="Add reaction" aria-label="Add reaction" + data-boundary="viewport" data-placement="bottom" type="button"> <span - v-html="emojiSmiling" - class="award-control-icon award-control-icon-neutral"> + class="award-control-icon award-control-icon-neutral" + v-html="emojiSmiling"> </span> <span - v-html="emojiSmiley" - class="award-control-icon award-control-icon-positive"> + class="award-control-icon award-control-icon-positive" + v-html="emojiSmiley"> </span> <span - v-html="emojiSmile" - class="award-control-icon award-control-icon-super-positive"> + class="award-control-icon award-control-icon-super-positive" + v-html="emojiSmile"> </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 0cb626c14f4..d2db68df98e 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -40,7 +40,7 @@ export default { this.initTaskList(); if (this.isEditing) { - this.initAutoSave(this.note.noteable_type); + this.initAutoSave(this.note); } }, updated() { @@ -49,7 +49,7 @@ export default { if (this.isEditing) { if (!this.autosave) { - this.initAutoSave(this.note.noteable_type); + this.initAutoSave(this.note); } else { this.setAutoSave(); } @@ -72,7 +72,7 @@ export default { this.$emit('handleFormUpdate', note, parentElement, callback); }, formCancelHandler(shouldConfirm, isDirty) { - this.$emit('cancelFormEdition', shouldConfirm, isDirty); + this.$emit('cancelForm', shouldConfirm, isDirty); }, }, }; @@ -80,20 +80,20 @@ export default { <template> <div - :class="{ 'js-task-list-container': canEdit }" ref="note-body" + :class="{ 'js-task-list-container': canEdit }" class="note-body"> <div - v-html="note.note_html" - class="note-text md"></div> + class="note-text md" + v-html="note.note_html"></div> <note-form v-if="isEditing" ref="noteForm" - @handleFormUpdate="handleFormUpdate" - @cancelFormEdition="formCancelHandler" :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" + @handleFormUpdate="handleFormUpdate" + @cancelForm="formCancelHandler" /> <textarea v-if="canEdit" @@ -105,6 +105,7 @@ export default { :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" action-text="Edited" + class="note_edited_ago" /> <note-awards-list v-if="note.award_emoji.length" diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index 2dc39d1a186..391bb2ae179 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -11,14 +11,20 @@ export default { type: String, required: true, }, + actionDetailText: { + type: String, + required: false, + default: '', + }, editedAt: { type: String, - required: true, + required: false, + default: null, }, editedBy: { type: Object, required: false, - default: () => ({}), + default: null, }, className: { type: String, @@ -33,13 +39,14 @@ export default { <div :class="className"> {{ actionText }} <template v-if="editedBy"> - {{ s__('ByAuthor|by') }} + by <a :href="editedBy.path" class="js-vue-author author_link"> {{ editedBy.name }} </a> </template> + {{ actionDetailText }} <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index c59a2e7a406..a4e3faa5d75 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -29,7 +29,7 @@ export default { required: false, default: 'Save comment', }, - note: { + discussion: { type: Object, required: false, default: () => ({}), @@ -38,6 +38,11 @@ export default { type: Boolean, required: true, }, + lineCode: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -66,9 +71,7 @@ export default { return this.getNotesDataByProp('markdownDocsPath'); }, quickActionsDocsPath() { - return !this.isEditing - ? this.getNotesDataByProp('quickActionsDocsPath') - : undefined; + return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined; }, currentUserId() { return this.getUserDataByProp('id'); @@ -95,24 +98,17 @@ export default { const beforeSubmitDiscussionState = this.discussionResolved; this.isSubmitting = true; - this.$emit( - 'handleFormUpdate', - this.updatedNoteBody, - this.$refs.editNoteForm, - () => { - this.isSubmitting = false; + this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { + this.isSubmitting = false; - if (shouldResolve) { - this.resolveHandler(beforeSubmitDiscussionState); - } - }, - ); + if (shouldResolve) { + this.resolveHandler(beforeSubmitDiscussionState); + } + }); }, editMyLastNote() { if (this.updatedNoteBody === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote( - this.updatedNoteBody, - ); + const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion); if (lastNoteInDiscussion) { eventHub.$emit('enterEditMode', { @@ -123,11 +119,7 @@ export default { }, cancelHandler(shouldConfirm = false) { // Sends information about confirm message and if the textarea has changed - this.$emit( - 'cancelFormEdition', - shouldConfirm, - this.noteBody !== this.updatedNoteBody, - ); + this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); }, }, }; @@ -136,7 +128,7 @@ export default { <template> <div ref="editNoteForm" - class="note-edit-form current-note-edit-form"> + class="note-edit-form current-note-edit-form js-discussion-note-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> @@ -150,7 +142,10 @@ export default { to ensure information is not lost. </div> <div class="flash-container timeline-content"></div> - <form class="edit-note common-note-form js-quick-submit gfm-form"> + <form + :data-line-code="lineCode" + class="edit-note common-note-form js-quick-submit gfm-form" + > <issue-warning v-if="hasWarning(getNoteableData)" @@ -165,15 +160,15 @@ export default { :add-spacing-classes="false"> <textarea id="note_note" + ref="textarea" + slot="textarea" + :data-supports-quick-actions="!isEditing" + v-model="updatedNoteBody" name="note[note]" - class="note-textarea js-gfm-input + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" - :data-supports-quick-actions="!isEditing" aria-label="Description" - v-model="updatedNoteBody" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files hereā¦" @keydown.meta.enter="handleUpdate()" @keydown.ctrl.enter="handleUpdate()" @keydown.up="editMyLastNote()" @@ -182,24 +177,24 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" </markdown-field> <div class="note-form-actions clearfix"> <button - type="button" - @click="handleUpdate()" :disabled="isDisabled" - class="js-vue-issue-save btn btn-save"> + type="button" + class="js-vue-issue-save btn btn-save js-comment-button " + @click="handleUpdate()"> {{ saveButtonTitle }} </button> <button - v-if="note.resolvable" - @click.prevent="handleUpdate(true)" + v-if="discussion.resolvable" class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} </button> <button - @click="cancelHandler()" - class="btn btn-cancel note-edit-cancel" - type="button"> - Cancel + class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" + type="button" + @click="cancelHandler()"> + {{ __('Discard draft') }} </button> </div> </form> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index a4081957207..ee3580895df 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -20,11 +20,6 @@ export default { required: false, default: '', }, - actionTextHtml: { - type: String, - required: false, - default: '', - }, noteId: { type: Number, required: true, @@ -66,9 +61,9 @@ export default { v-if="includeToggle" class="discussion-actions"> <button - @click="handleToggle" class="note-action-button discussion-toggle-button js-vue-toggle-button" - type="button"> + type="button" + @click="handleToggle"> <i :class="toggleChevronClass" class="fa" @@ -88,18 +83,16 @@ export default { <template v-if="actionText"> {{ actionText }} </template> - <span - v-if="actionTextHtml" - v-html="actionTextHtml" - class="system-note-message"> + <span class="system-note-message"> + <slot></slot> </span> <span class="system-note-separator"> · </span> <a :href="noteTimestampLink" - @click="updateTargetNoteHash" - class="note-timestamp system-note-separator"> + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 7f5aacaa3a2..bee635398b3 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,7 +1,11 @@ <script> +import _ from 'underscore'; import { mapActions, mapGetters } from 'vuex'; import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; import nextDiscussionsSvg from 'icons/_next_discussion.svg'; +import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils'; +import { truncateSha } from '~/lib/utils/text_utility'; +import systemNote from '~/vue_shared/components/notes/system_note.vue'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -17,9 +21,9 @@ import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import tooltip from '../../vue_shared/directives/tooltip'; -import { scrollToElement } from '../../lib/utils/common_utils'; export default { + name: 'NoteableDiscussion', components: { noteableNote, diffWithNote, @@ -30,16 +34,32 @@ export default { noteForm, placeholderNote, placeholderSystemNote, + systemNote, }, directives: { tooltip, }, mixins: [autosave, noteable, resolvable], props: { - note: { + discussion: { type: Object, required: true, }, + renderHeader: { + type: Boolean, + required: false, + default: true, + }, + renderDiffFile: { + type: Boolean, + required: false, + default: true, + }, + alwaysExpanded: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -53,19 +73,27 @@ export default { 'getNoteableData', 'discussionCount', 'resolvedDiscussionCount', + 'allDiscussions', 'unresolvedDiscussions', ]), - discussion() { + transformedDiscussion() { return { - ...this.note.notes[0], - truncatedDiffLines: this.note.truncated_diff_lines, - diffFile: this.note.diff_file, - diffDiscussion: this.note.diff_discussion, - imageDiffHtml: this.note.image_diff_html, + ...this.discussion.notes[0], + truncatedDiffLines: this.discussion.truncated_diff_lines || [], + truncatedDiffLinesPath: this.discussion.truncated_diff_lines_path, + diffFile: this.discussion.diff_file, + diffDiscussion: this.discussion.diff_discussion, + imageDiffHtml: this.discussion.image_diff_html, + active: this.discussion.active, + discussionPath: this.discussion.discussion_path, + resolved: this.discussion.resolved, + resolvedBy: this.discussion.resolved_by, + resolvedByPush: this.discussion.resolved_by_push, + resolvedAt: this.discussion.resolved_at, }; }, author() { - return this.discussion.author; + return this.transformedDiscussion.author; }, canReply() { return this.getNoteableData.current_user.can_create_note; @@ -74,7 +102,7 @@ export default { return this.getNoteableData.create_note_path; }, lastUpdatedBy() { - const { notes } = this.note; + const { notes } = this.discussion; if (notes.length > 1) { return notes[notes.length - 1].author; @@ -83,7 +111,7 @@ export default { return null; }, lastUpdatedAt() { - const { notes } = this.note; + const { notes } = this.discussion; if (notes.length > 1) { return notes[notes.length - 1].created_at; @@ -91,27 +119,40 @@ export default { return null; }, - hasUnresolvedDiscussion() { - return this.unresolvedDiscussions.length > 0; + resolvedText() { + return this.transformedDiscussion.resolvedByPush ? 'Automatically resolved' : 'Resolved'; + }, + hasMultipleUnresolvedDiscussions() { + return this.unresolvedDiscussions.length > 1; + }, + shouldRenderDiffs() { + const { diffDiscussion, diffFile } = this.transformedDiscussion; + + return diffDiscussion && diffFile && this.renderDiffFile; }, wrapperComponent() { - return this.discussion.diffDiscussion && this.discussion.diffFile - ? diffWithNote - : 'div'; + return this.shouldRenderDiffs ? diffWithNote : 'div'; + }, + wrapperComponentProps() { + if (this.shouldRenderDiffs) { + return { discussion: convertObjectPropsToCamelCase(this.discussion) }; + } + + return {}; }, wrapperClass() { - return this.isDiffDiscussion ? '' : 'card'; + return this.isDiffDiscussion ? '' : 'card discussion-wrapper'; }, }, mounted() { if (this.isReplying) { - this.initAutoSave(this.discussion.noteable_type); + this.initAutoSave(this.transformedDiscussion); } }, updated() { if (this.isReplying) { if (!this.autosave) { - this.initAutoSave(this.discussion.noteable_type); + this.initAutoSave(this.transformedDiscussion); } else { this.setAutoSave(); } @@ -127,7 +168,9 @@ export default { 'toggleDiscussion', 'removePlaceholderNotes', 'toggleResolveNote', + 'expandDiscussion', ]), + truncateSha, componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { @@ -136,23 +179,25 @@ export default { return placeholderNote; } + if (note.system) { + return systemNote; + } + return noteableNote; }, componentData(note) { - return note.isPlaceholderNote ? this.note.notes[0] : note; + return note.isPlaceholderNote ? this.discussion.notes[0] : note; }, toggleDiscussionHandler() { - this.toggleDiscussion({ discussionId: this.note.id }); + this.toggleDiscussion({ discussionId: this.discussion.id }); }, showReplyForm() { this.isReplying = true; }, cancelReplyForm(shouldConfirm) { if (shouldConfirm && this.$refs.noteForm.isDirty) { - const msg = 'Are you sure you want to cancel creating this comment?'; - // eslint-disable-next-line no-alert - if (!confirm(msg)) { + if (!window.confirm('Are you sure you want to cancel creating this comment?')) { return; } } @@ -161,18 +206,23 @@ export default { this.isReplying = false; }, saveReply(noteText, form, callback) { + const postData = { + in_reply_to_discussion_id: this.discussion.reply_id, + target_type: this.getNoteableData.targetType, + note: { note: noteText }, + }; + + if (this.discussion.for_commit) { + postData.note_project_id = this.discussion.project_id; + } + const replyData = { endpoint: this.newNotePath, flashContainer: this.$el, - data: { - in_reply_to_discussion_id: this.note.reply_id, - target_type: this.noteableType, - target_id: this.discussion.noteable_id, - note: { note: noteText }, - }, + data: postData, }; - this.isReplying = false; + this.isReplying = false; this.saveNote(replyData) .then(() => { this.resetAutoSave(); @@ -190,15 +240,19 @@ Please check your network connection and try again.`; }); }); }, - jumpToDiscussion() { + jumpToNextDiscussion() { + const discussionIds = this.allDiscussions.map(d => d.id); const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); - const index = unresolvedIds.indexOf(this.note.id); + const currentIndex = discussionIds.indexOf(this.discussion.id); + const remainingAfterCurrent = discussionIds.slice(currentIndex + 1); + const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1); - if (index >= 0 && index !== unresolvedIds.length) { - const nextId = unresolvedIds[index + 1]; + if (nextIndex > -1) { + const nextId = remainingAfterCurrent[nextIndex]; const el = document.querySelector(`[data-discussion-id="${nextId}"]`); if (el) { + this.expandDiscussion({ discussionId: nextId }); scrollToElement(el); } } @@ -208,9 +262,7 @@ Please check your network connection and try again.`; </script> <template> - <li - :data-discussion-id="note.id" - class="note note-discussion timeline-entry"> + <li class="note note-discussion timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -221,20 +273,52 @@ Please check your network connection and try again.`; /> </div> <div class="timeline-content"> - <div class="discussion"> - <div class="discussion-header"> + <div + :data-discussion-id="transformedDiscussion.discussion_id" + class="discussion js-discussion-container" + > + <div + v-if="renderHeader" + class="discussion-header" + > <note-header :author="author" - :created-at="discussion.created_at" - :note-id="discussion.id" + :created-at="transformedDiscussion.created_at" + :note-id="transformedDiscussion.id" :include-toggle="true" - :expanded="note.expanded" + :expanded="discussion.expanded" @toggleHandler="toggleDiscussionHandler" - action-text="started a discussion" - class="discussion" + > + <template v-if="transformedDiscussion.diffDiscussion"> + started a discussion on + <a :href="transformedDiscussion.discussionPath"> + <template v-if="transformedDiscussion.active"> + the diff + </template> + <template v-else> + an old version of the diff + </template> + </a> + </template> + <template v-else-if="discussion.for_commit"> + started a discussion on commit + <a :href="discussion.discussion_path"> + {{ truncateSha(discussion.commit_id) }} + </a> + </template> + <template v-else> + started a discussion + </template> + </note-header> + <note-edited-text + v-if="transformedDiscussion.resolved" + :edited-at="transformedDiscussion.resolvedAt" + :edited-by="transformedDiscussion.resolvedBy" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline" /> <note-edited-text - v-if="lastUpdatedAt" + v-else-if="lastUpdatedAt" :edited-at="lastUpdatedAt" :edited-by="lastUpdatedBy" action-text="Last updated" @@ -242,17 +326,17 @@ Please check your network connection and try again.`; /> </div> <div - v-if="note.expanded" + v-if="discussion.expanded || alwaysExpanded" class="discussion-body"> <component :is="wrapperComponent" - :discussion="discussion" + v-bind="wrapperComponentProps" :class="wrapperClass" > <div class="discussion-notes"> <ul class="notes"> <component - v-for="note in note.notes" + v-for="note in discussion.notes" :is="componentName(note)" :note="componentData(note)" :key="note.id" @@ -260,28 +344,29 @@ Please check your network connection and try again.`; </ul> <div :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder"> + 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" + class="btn-group w-100" role="group"> <button - @click="showReplyForm" type="button" - class="js-vue-discussion-reply btn btn-text-field" - title="Add a reply">Reply...</button> + class="js-vue-discussion-reply btn btn-text-field mr-2" + title="Add a reply" + @click="showReplyForm">Reply...</button> </div> <div - v-if="note.resolvable" + v-if="discussion.resolvable" class="btn-group" role="group"> <button - @click="resolveHandler()" type="button" - class="btn btn-default" + class="btn btn-default mr-2" + @click="resolveHandler()" > <i v-if="isResolving" @@ -292,7 +377,7 @@ Please check your network connection and try again.`; </button> </div> <div - v-if="note.resolvable" + v-if="discussion.resolvable" class="btn-group discussion-actions" role="group" > @@ -301,26 +386,26 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <a - :href="note.resolve_with_issue_path" v-tooltip + :href="discussion.resolve_with_issue_path" + :title="s__('MergeRequests|Resolve this discussion in a new issue')" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" - title="Resolve this discussion in a new issue" data-container="body" > <span v-html="resolveDiscussionsSvg"></span> </a> </div> <div - v-if="hasUnresolvedDiscussion" + v-if="hasMultipleUnresolvedDiscussions" class="btn-group" role="group"> <button - @click="jumpToDiscussion" v-tooltip class="btn btn-default discussion-next-btn" title="Jump to next unresolved discussion" data-container="body" + @click="jumpToNextDiscussion" > <span v-html="nextDiscussionsSvg"></span> </button> @@ -330,12 +415,12 @@ Please check your network connection and try again.`; </template> <note-form v-if="isReplying" - save-button-title="Comment" - :note="note" + ref="noteForm" + :discussion="discussion" :is-editing="false" + save-button-title="Comment" @handleFormUpdate="saveReply" - @cancelFormEdition="cancelReplyForm" - ref="noteForm" /> + @cancelForm="cancelReplyForm" /> <note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 566f5c68e66..4ebeb5599f2 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -12,6 +12,7 @@ import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; export default { + name: 'NoteableNote', components: { userAvatarLink, noteHeader, @@ -34,26 +35,31 @@ export default { }; }, computed: { - ...mapGetters(['targetNoteHash', 'getUserData']), + ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']), author() { return this.note.author; }, classNameBindings() { return { + [`note-row-${this.note.id}`]: true, 'is-editing': this.isEditing && !this.isRequesting, 'is-requesting being-posted': this.isRequesting, 'disabled-content': this.isDeleting, - target: this.targetNoteHash === this.noteAnchorId, + target: this.isTarget, }; }, + canResolve() { + return this.note.resolvable && !!this.getUserData.id; + }, canReportAsAbuse() { - return ( - this.note.report_abuse_path && this.author.id !== this.getUserData.id - ); + return this.note.report_abuse_path && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; }, + isTarget() { + return this.targetNoteHash === this.noteAnchorId; + }, }, created() { @@ -65,19 +71,20 @@ export default { }); }, + mounted() { + if (this.isTarget) { + this.scrollToNoteIfNeeded($(this.$el)); + } + }, + methods: { - ...mapActions([ - 'deleteNote', - 'updateNote', - 'toggleResolveNote', - 'scrollToNoteIfNeeded', - ]), + ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']), editHandler() { this.isEditing = true; }, deleteHandler() { // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to delete this comment?')) { + if (window.confirm('Are you sure you want to delete this comment?')) { this.isDeleting = true; this.deleteNote(this.note) @@ -85,9 +92,7 @@ export default { this.isDeleting = false; }) .catch(() => { - Flash( - 'Something went wrong while deleting your note. Please try again.', - ); + Flash('Something went wrong while deleting your note. Please try again.'); this.isDeleting = false; }); } @@ -96,7 +101,7 @@ export default { const data = { endpoint: this.note.path, note: { - target_type: this.noteableType, + target_type: this.getNoteableData.targetType, target_id: this.note.noteable_id, note: { note: noteText }, }, @@ -118,8 +123,7 @@ export default { this.isRequesting = false; this.isEditing = true; this.$nextTick(() => { - const msg = - 'Something went wrong while editing your comment. Please try again.'; + const msg = 'Something went wrong while editing your comment. Please try again.'; Flash(msg, 'alert', this.$el); this.recoverNoteContent(noteText); callback(); @@ -129,8 +133,7 @@ export default { formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel editing this comment?')) - return; + if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { @@ -143,7 +146,7 @@ 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.$refs.noteForm.note.note = noteText; + this.$refs.noteBody.note.note = noteText; }, }, }; @@ -151,10 +154,12 @@ export default { <template> <li - class="note timeline-entry" :id="noteAnchorId" :class="classNameBindings" - :data-award-url="note.toggle_award_path"> + :data-award-url="note.toggle_award_path" + :data-note-id="note.id" + class="note timeline-entry" + > <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -170,16 +175,17 @@ export default { :author="author" :created-at="note.created_at" :note-id="note.id" - action-text="commented" /> <note-actions :author-id="author.id" :note-id="note.id" + :note-url="note.noteable_note_url" :access-level="note.human_access" :can-edit="note.current_user.can_edit" :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" :report-abuse-path="note.report_abuse_path" :resolvable="note.resolvable" :is-resolved="note.resolved" @@ -191,12 +197,12 @@ export default { /> </div> <note-body + ref="noteBody" :note="note" :can-edit="note.current_user.can_edit" :is-editing="isEditing" @handleFormUpdate="formUpdateHandler" - @cancelFormEdition="formCancelHandler" - ref="noteBody" + @cancelForm="formCancelHandler" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index ebfc827ac57..a8995021699 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,10 +1,9 @@ <script> -import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { getLocationHash } from '../../lib/utils/url_utility'; import Flash from '../../flash'; -import store from '../stores/'; import * as constants from '../constants'; +import eventHub from '../event_hub'; import noteableNote from './noteable_note.vue'; import noteableDiscussion from './noteable_discussion.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue'; @@ -39,19 +38,23 @@ export default { required: false, default: () => ({}), }, + shouldShow: { + type: Boolean, + required: false, + default: true, + }, }, - store, data() { return { isLoading: true, }; }, computed: { - ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), + ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']), noteableType() { return this.noteableData.noteableType; }, - allNotes() { + allDiscussions() { if (this.isLoading) { const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0; @@ -59,36 +62,40 @@ export default { isSkeletonNote: true, }); } - return this.notes; + + return this.discussions; + }, + }, + watch: { + shouldShow() { + if (!this.isNotesFetched) { + this.fetchNotes(); + } }, }, created() { this.setNotesData(this.notesData); this.setNoteableData(this.noteableData); this.setUserData(this.userData); + this.setTargetNoteHash(getLocationHash()); + eventHub.$once('fetchNotesData', this.fetchNotes); }, mounted() { - this.fetchNotes(); - - const parentElement = this.$el.parentElement; + if (this.shouldShow) { + this.fetchNotes(); + } - if ( - parentElement && - parentElement.classList.contains('js-vue-notes-event') - ) { + const { parentElement } = this.$el; + if (parentElement && parentElement.classList.contains('js-vue-notes-event')) { parentElement.addEventListener('toggleAward', event => { const { awardName, noteId } = event.detail; this.actionToggleAward({ awardName, noteId }); }); } - document.addEventListener('refreshVueNotes', this.fetchNotes); - }, - beforeDestroy() { - document.removeEventListener('refreshVueNotes', this.fetchNotes); }, methods: { ...mapActions({ - actionFetchNotes: 'fetchNotes', + fetchDiscussions: 'fetchDiscussions', poll: 'poll', actionToggleAward: 'toggleAward', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', @@ -97,38 +104,42 @@ export default { setUserData: 'setUserData', setLastFetchedAt: 'setLastFetchedAt', setTargetNoteHash: 'setTargetNoteHash', + toggleDiscussion: 'toggleDiscussion', + setNotesFetchedState: 'setNotesFetchedState', }), - getComponentName(note) { - if (note.isSkeletonNote) { + getComponentName(discussion) { + if (discussion.isSkeletonNote) { return skeletonLoadingContainer; } - if (note.isPlaceholderNote) { - if (note.placeholderType === constants.SYSTEM_NOTE) { + if (discussion.isPlaceholderNote) { + if (discussion.placeholderType === constants.SYSTEM_NOTE) { return placeholderSystemNote; } return placeholderNote; - } else if (note.individual_note) { - return note.notes[0].system ? systemNote : noteableNote; + } else if (discussion.individual_note) { + return discussion.notes[0].system ? systemNote : noteableNote; } return noteableDiscussion; }, - getComponentData(note) { - return note.individual_note ? note.notes[0] : note; + getComponentData(discussion) { + return discussion.individual_note ? { note: discussion.notes[0] } : { discussion }; }, fetchNotes() { - return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) - .then(() => this.initPolling()) + return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath')) + .then(() => { + this.initPolling(); + }) .then(() => { this.isLoading = false; + this.setNotesFetchedState(true); }) .then(() => this.$nextTick()) .then(() => this.checkLocationHash()) .catch(() => { this.isLoading = false; - Flash( - 'Something went wrong while fetching comments. Please try again.', - ); + this.setNotesFetchedState(true); + Flash('Something went wrong while fetching comments. Please try again.'); }); }, initPolling() { @@ -143,11 +154,19 @@ export default { }, checkLocationHash() { const hash = getLocationHash(); - const element = document.getElementById(hash); + const noteId = hash && hash.replace(/^note_/, ''); - if (hash && element) { - this.setTargetNoteHash(hash); - this.scrollToNoteIfNeeded($(element)); + if (noteId) { + this.discussions.forEach(discussion => { + if (discussion.notes) { + discussion.notes.forEach(note => { + if (`${note.id}` === `${noteId}`) { + // FIXME: this modifies the store state without using a mutation/action + Object.assign(discussion, { expanded: true }); + } + }); + } + }); } }, }, @@ -155,16 +174,19 @@ export default { </script> <template> - <div id="notes"> + <div + v-show="shouldShow" + id="notes" + > <ul id="notes-list" - class="notes main-notes-list timeline"> - + class="notes main-notes-list timeline" + > <component - v-for="note in allNotes" - :is="getComponentName(note)" - :note="getComponentData(note)" - :key="note.id" + v-for="discussion in allDiscussions" + :is="getComponentName(discussion)" + v-bind="getComponentData(discussion)" + :key="discussion.id" /> </ul> |