summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/notes
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/notes')
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue278
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue124
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue50
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue18
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue100
-rw-r--r--app/assets/javascripts/notes/components/note_attachment.vue23
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue27
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue17
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue57
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue45
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue429
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue101
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue114
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue35
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js53
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js28
-rw-r--r--app/assets/javascripts/notes/stores/getters.js65
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js45
24 files changed, 727 insertions, 909 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 10e80883c00..ce56beb1e6b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import Autosave from '../../autosave';
import {
@@ -30,6 +31,7 @@ export default {
markdownField,
userAvatarLink,
loadingButton,
+ TimelineEntryItem,
},
mixins: [issuableStateMixin],
props: {
@@ -245,15 +247,19 @@ Please check your network connection and try again.`;
} else {
this.reopenIssue()
.then(() => this.enableButton())
- .catch(() => {
+ .catch(({ data }) => {
this.enableButton();
this.toggleStateButtonLoading(false);
- Flash(
- sprintf(
- __('Something went wrong while reopening the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
- ),
+ let errorMessage = sprintf(
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
);
+
+ if (data) {
+ errorMessage = Object.values(data).join('\n');
+ }
+
+ Flash(errorMessage);
});
}
},
@@ -308,152 +314,136 @@ Please check your network connection and try again.`;
<template>
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
- <discussion-locked-widget
- v-else-if="!canCreateNote"
- :issuable-type="issuableTypeTitle"
- />
- <div
- v-else-if="canCreateNote"
- class="notes notes-form timeline">
- <div class="timeline-entry note-form">
- <div class="timeline-entry-inner">
- <div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon d-none d-sm-none d-md-block">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <div class="timeline-content timeline-content-form">
- <form
- ref="commentForm"
- class="new-note common-note-form gfm-form js-main-target-form"
- >
-
- <div class="error-alert"></div>
+ <discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" />
+ <ul v-else-if="canCreateNote" class="notes notes-form timeline">
+ <timeline-entry-item class="note-form">
+ <div class="flash-container error-alert timeline-content"></div>
+ <div class="timeline-icon d-none d-sm-none d-md-block">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="timeline-content timeline-content-form">
+ <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
+ <div class="error-alert"></div>
- <issue-warning
- v-if="hasWarning(getNoteableData)"
- :is-locked="isLocked(getNoteableData)"
- :is-confidential="isConfidential(getNoteableData)"
- />
+ <issue-warning
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
+ />
- <markdown-field
- ref="markdownField"
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
- :markdown-version="markdownVersion"
- :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 js-note-text
+ <markdown-field
+ ref="markdownField"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :markdown-version="markdownVersion"
+ :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 js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
- data-supports-quick-actions="true"
- aria-label="Description"
- placeholder="Write a comment or drag your files here…"
- @keydown.up="editCurrentUserLastNote()"
- @keydown.meta.enter="handleSave()"
- @keydown.ctrl.enter="handleSave()">
- </textarea>
- </markdown-field>
- <div class="note-form-actions">
- <div
- class="float-left btn-group
-append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
- <button
- :disabled="isSubmitButtonDisabled"
- class="btn btn-create comment-btn js-comment-button js-comment-submit-button
+ data-supports-quick-actions="true"
+ aria-label="Description"
+ placeholder="Write a comment or drag your files here…"
+ @keydown.up="editCurrentUserLastNote();"
+ @keydown.meta.enter="handleSave();"
+ @keydown.ctrl.enter="handleSave();"
+ >
+ </textarea>
+ </markdown-field>
+ <div class="note-form-actions">
+ <div
+ class="float-left btn-group
+append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
+ >
+ <button
+ :disabled="isSubmitButtonDisabled"
+ class="btn btn-create comment-btn js-comment-button js-comment-submit-button
qa-comment-button"
- type="submit"
- @click.prevent="handleSave()">
- {{ __(commentButtonTitle) }}
- </button>
- <button
- :disabled="isSubmitButtonDisabled"
- name="button"
- type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
- data-display="static"
- data-toggle="dropdown"
- aria-label="Open comment type dropdown">
- <i
- aria-hidden="true"
- class="fa fa-caret-down toggle-icon">
- </i>
- </button>
-
- <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
- <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
- <button
- type="button"
- class="btn btn-transparent"
- @click.prevent="setNoteType('comment')">
- <i
- aria-hidden="true"
- class="fa fa-check icon">
- </i>
- <div class="description">
- <strong>Comment</strong>
- <p>
- Add a general comment to this {{ noteableDisplayName }}.
- </p>
- </div>
- </button>
- </li>
- <li class="divider droplab-item-ignore"></li>
- <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
- <button
- type="button"
- class="btn btn-transparent qa-discussion-option"
- @click.prevent="setNoteType('discussion')">
- <i
- aria-hidden="true"
- class="fa fa-check icon">
- </i>
- <div class="description">
- <strong>Start discussion</strong>
- <p>
- {{ startDiscussionDescription }}
- </p>
- </div>
- </button>
- </li>
- </ul>
- </div>
-
- <loading-button
- v-if="canUpdateIssue"
- :loading="isToggleStateButtonLoading"
- :container-class="[
- actionButtonClassNames,
- 'btn btn-comment btn-comment-and-close js-action-button'
- ]"
- :disabled="isToggleStateButtonLoading || isSubmitting"
- :label="issueActionButtonTitle"
- @click="handleSave(true)"
- />
-
+ type="submit"
+ @click.prevent="handleSave();"
+ >
+ {{ __(commentButtonTitle) }}
+ </button>
<button
- v-if="note.length"
+ :disabled="isSubmitButtonDisabled"
+ name="button"
type="button"
- class="btn btn-cancel js-note-discard"
- @click="discard">
- Discard draft
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
+ data-display="static"
+ data-toggle="dropdown"
+ aria-label="Open comment type dropdown"
+ >
+ <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
</button>
+
+ <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
+ <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
+ <button
+ type="button"
+ class="btn btn-transparent"
+ @click.prevent="setNoteType('comment');"
+ >
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>Comment</strong>
+ <p>Add a general comment to this {{ noteableDisplayName }}.</p>
+ </div>
+ </button>
+ </li>
+ <li class="divider droplab-item-ignore"></li>
+ <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
+ <button
+ type="button"
+ class="btn btn-transparent qa-discussion-option"
+ @click.prevent="setNoteType('discussion');"
+ >
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>Start discussion</strong>
+ <p>{{ startDiscussionDescription }}</p>
+ </div>
+ </button>
+ </li>
+ </ul>
</div>
- </form>
- </div>
+
+ <loading-button
+ v-if="canUpdateIssue"
+ :loading="isToggleStateButtonLoading"
+ :container-class="[
+ actionButtonClassNames,
+ 'btn btn-comment btn-comment-and-close js-action-button',
+ ]"
+ :disabled="isToggleStateButtonLoading || isSubmitting"
+ :label="issueActionButtonTitle"
+ @click="handleSave(true);"
+ />
+
+ <button
+ v-if="note.length"
+ type="button"
+ class="btn btn-cancel js-note-discard"
+ @click="discard"
+ >
+ Discard draft
+ </button>
+ </div>
+ </form>
</div>
- </div>
- </div>
+ </timeline-entry-item>
+ </ul>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 080161dfbba..af821df0fd2 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -3,8 +3,10 @@ import { mapState, mapActions } from 'vuex';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
-import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
-import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
+import { GlSkeletonLoading } from '@gitlab/ui';
+import { getDiffMode } from '~/diffs/store/utils';
+
+const FIRST_CHAR_REGEX = /^(\+|-| )/;
export default {
components: {
@@ -26,46 +28,16 @@ export default {
},
computed: {
...mapState({
- noteableData: state => state.notes.noteableData,
projectPath: state => state.diffs.projectPath,
}),
diffMode() {
- return getDiffMode(this.diffFile);
+ return getDiffMode(this.discussion.diff_file);
},
hasTruncatedDiffLines() {
return (
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
);
},
- isDiscussionsExpanded() {
- return true; // TODO: @fatihacet - Fix this.
- },
- isCollapsed() {
- return this.diffFile.collapsed || false;
- },
- isImageDiff() {
- return !this.diffFile.text;
- },
- diffFileClass() {
- const { text } = this.diffFile;
- return text ? 'text-file' : 'js-image-file';
- },
- diffFile() {
- return this.discussion.diff_file;
- },
- imageDiffHtml() {
- return this.discussion.image_diff_html;
- },
- userColorScheme() {
- return window.gon.user_color_scheme;
- },
- normalizedDiffLines() {
- if (this.discussion.truncated_diff_lines) {
- return this.discussion.truncated_diff_lines.map(line => trimFirstCharOfLineContent(line));
- }
-
- return [];
- },
},
mounted() {
if (!this.hasTruncatedDiffLines) {
@@ -74,9 +46,6 @@ export default {
},
methods: {
...mapActions(['fetchDiscussionDiffLines']),
- rowTag(html) {
- return html.outerHTML ? 'tr' : 'template';
- },
fetchDiff() {
this.error = false;
this.fetchDiscussionDiffLines(this.discussion)
@@ -85,54 +54,45 @@ export default {
this.error = true;
});
},
+ trimChar(line) {
+ return line.replace(FIRST_CHAR_REGEX, '');
+ },
},
+ userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
<template>
- <div
- ref="fileHolder"
- :class="diffFileClass"
- class="diff-file file-holder"
- >
+ <div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder">
<diff-file-header
:discussion-path="discussion.discussion_path"
- :diff-file="diffFile"
+ :diff-file="discussion.diff_file"
:can-current-user-fork="false"
- :discussions-expanded="isDiscussionsExpanded"
- :expanded="!isCollapsed"
+ :expanded="!discussion.diff_file.collapsed"
/>
<div
- v-if="diffFile.text"
- :class="userColorScheme"
+ v-if="discussion.diff_file.text"
+ :class="$options.userColorSchemeClass"
class="diff-content code"
>
<table>
- <tr
- v-for="line in normalizedDiffLines"
- :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="line_content"
- v-html="line.rich_text"
+ <template v-if="hasTruncatedDiffLines">
+ <tr
+ v-for="line in discussion.truncated_diff_lines"
+ v-once
+ :key="line.line_code"
+ class="line_holder"
>
- </td>
- </tr>
- <tr
- v-if="!hasTruncatedDiffLines"
- class="line_holder line-holder-placeholder"
- >
+ <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="line_content" v-html="trimChar(line.rich_text)"></td>
+ </tr>
+ </template>
+ <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
+ <td v-if="error" class="js-error-lazy-load-diff diff-loading-error-block">
+ {{ error }} Unable to load the diff
<button
class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
@click="fetchDiff"
@@ -140,41 +100,31 @@ export default {
Try again
</button>
</td>
- <td
- v-else
- class="line_content js-success-lazy-load"
- >
+ <td v-else class="line_content js-success-lazy-load">
<span></span>
<gl-skeleton-loading />
<span></span>
</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>
- <div
- v-else
- >
+ <div v-else>
<diff-viewer
:diff-mode="diffMode"
- :new-path="diffFile.new_path"
- :new-sha="diffFile.diff_refs.head_sha"
- :old-path="diffFile.old_path"
- :old-sha="diffFile.diff_refs.base_sha"
- :file-hash="diffFile.file_hash"
+ :new-path="discussion.diff_file.new_path"
+ :new-sha="discussion.diff_file.diff_refs.head_sha"
+ :old-path="discussion.diff_file.old_path"
+ :old-sha="discussion.diff_file.diff_refs.base_sha"
+ :file-hash="discussion.diff_file.file_hash"
:project-path="projectPath"
>
<image-diff-overlay
slot="image-overlay"
:discussions="discussion"
- :file-hash="diffFile.file_hash"
+ :file-hash="discussion.diff_file.file_hash"
:show-comment-icon="true"
:should-toggle-discussion="false"
badge-class="image-comment-badge"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index a4d76a70696..c7cfc0f0f3b 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,13 +1,12 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
Icon,
@@ -17,9 +16,9 @@ export default {
...mapGetters([
'getUserData',
'getNoteableData',
- 'discussionCount',
+ 'resolvableDiscussionsCount',
'firstUnresolvedDiscussionId',
- 'resolvedDiscussionCount',
+ 'unresolvedDiscussionsCount',
]),
isLoggedIn() {
return this.getUserData.id;
@@ -27,15 +26,15 @@ export default {
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
- countText() {
- return pluralize('discussion', this.discussionCount);
- },
allResolved() {
- return this.resolvedDiscussionCount === this.discussionCount;
+ return this.unresolvedDiscussionsCount === 0;
},
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
+ resolvedDiscussionsCount() {
+ return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
+ },
},
methods: {
...mapActions(['expandDiscussion']),
@@ -50,13 +49,9 @@ export default {
</script>
<template>
- <div
- v-if="discussionCount > 0"
- class="line-resolve-all-container prepend-top-8">
+ <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container prepend-top-8">
<div>
- <div
- :class="{ 'has-next-btn': hasNextButton }"
- class="line-resolve-all">
+ <div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all">
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
@@ -65,32 +60,27 @@ export default {
<icon name="check-circle" />
</span>
<span class="line-resolve-text">
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
+ {{ 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" role="group">
<a
- v-tooltip
+ v-gl-tooltip
: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">
+ class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
+ >
<icon name="issue-new" />
</a>
</div>
- <div
- v-if="isLoggedIn && !allResolved"
- class="btn-group"
- role="group">
+ <div v-if="isLoggedIn && !allResolved" class="btn-group" role="group">
<button
- v-tooltip
+ v-gl-tooltip
title="Jump to first unresolved discussion"
- data-container="body"
class="btn btn-default discussion-next-btn"
- @click="jumpToFirstUnresolvedDiscussion">
+ @click="jumpToFirstUnresolvedDiscussion"
+ >
<icon name="comment-next" />
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index affa2d1b574..86c114a761a 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -64,30 +64,24 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- {{ currentFilter.title }}
- <icon name="chevron-down" />
+ {{ currentFilter.title }} <icon name="chevron-down" />
</button>
<div
class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
- aria-labelledby="discussion-filter-dropdown">
+ 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">
<button
:class="{ 'is-active': filter.value === currentValue }"
class="qa-filter-options"
type="button"
- @click="selectFilter(filter.value)"
+ @click="selectFilter(filter.value);"
>
{{ filter.title }}
</button>
- <div
- v-if="filter.value === defaultValue"
- class="dropdown-divider"
- ></div>
+ <div v-if="filter.value === defaultValue" class="dropdown-divider"></div>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index de0a5f8489b..c469a6b7bcd 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -13,11 +13,7 @@ export default {
<template>
<div class="disabled-comment text-center">
<span class="issuable-note-warning inline">
- <icon
- :size="16"
- name="lock"
- class="icon"
- />
+ <icon :size="16" name="lock" class="icon" />
<span>
This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.
</span>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index f7a61fbfcd4..d99694b06e9 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,8 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
-import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
+import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
export default {
name: 'NoteActions',
@@ -11,7 +10,7 @@ export default {
GlLoadingIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
authorId: {
@@ -119,116 +118,76 @@ export default {
<template>
<div class="note-actions">
- <span
- v-if="accessLevel"
- class="note-role user-access-role">
- {{ accessLevel }}
- </span>
- <div
- v-if="canResolve"
- class="note-actions-item">
+ <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
+ <div v-if="canResolve" class="note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
type="button"
class="line-resolve-btn note-action-button"
- @click="onResolve">
+ @click="onResolve"
+ >
<template v-if="!isResolving">
<icon name="check-circle" />
</template>
- <gl-loading-icon
- v-else
- inline
- />
+ <gl-loading-icon v-else inline />
</button>
</div>
- <div
- v-if="canAwardEmoji"
- class="note-actions-item">
+ <div v-if="canAwardEmoji" class="note-actions-item">
<a
- v-tooltip
+ v-gl-tooltip.bottom
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
class="note-action-button note-emoji-button js-add-award js-note-emoji"
data-position="right"
- data-placement="bottom"
- data-container="body"
href="#"
title="Add reaction"
>
- <gl-loading-icon inline/>
+ <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-positive" name="emoji_smiley" />
+ <icon css-classes="link-highlight award-control-icon-super-positive" name="emoji_smiley" />
</a>
</div>
- <div
- v-if="canEdit"
- class="note-actions-item">
+ <div v-if="canEdit" class="note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip.bottom
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
- data-container="body"
- data-placement="bottom"
- @click="onEdit">
- <icon
- name="pencil"
- css-classes="link-highlight"
- />
+ @click="onEdit"
+ >
+ <icon name="pencil" css-classes="link-highlight" />
</button>
</div>
- <div
- v-if="showDeleteAction"
- class="note-actions-item"
- >
+ <div v-if="showDeleteAction" class="note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip.bottom
type="button"
title="Delete comment"
class="note-action-button js-note-delete btn btn-transparent"
- data-container="body"
- data-placement="bottom"
@click="onDelete"
>
- <icon
- name="remove"
- class="link-highlight"
- />
+ <icon name="remove" class="link-highlight" />
</button>
</div>
- <div
- v-else-if="shouldShowActionsDropdown"
- class="dropdown more-actions note-actions-item">
+ <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip.bottom
type="button"
title="More actions"
class="note-action-button more-actions-toggle btn btn-transparent"
data-toggle="dropdown"
- data-container="body"
- data-placement="bottom">
- <icon
- css-classes="icon"
- name="ellipsis_v"
- />
+ >
+ <icon css-classes="icon" name="ellipsis_v" />
</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 GitLab') }}</a>
</li>
<li v-if="noteUrl">
<button
@@ -243,10 +202,9 @@ export default {
<button
class="btn btn-transparent js-note-delete js-note-delete"
type="button"
- @click.prevent="onDelete">
- <span class="text-danger">
- {{ __('Delete comment') }}
- </span>
+ @click.prevent="onDelete"
+ >
+ <span class="text-danger">{{ __('Delete comment') }}</span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue
index 34ecbd00c63..b6d8c831e2e 100644
--- a/app/assets/javascripts/notes/components/note_attachment.vue
+++ b/app/assets/javascripts/notes/components/note_attachment.vue
@@ -12,27 +12,12 @@ export default {
<template>
<div class="note-attachment">
- <a
- v-if="attachment.image"
- :href="attachment.url"
- target="_blank"
- rel="noopener noreferrer">
- <img
- :src="attachment.url"
- class="note-image-attach"
- />
+ <a v-if="attachment.image" :href="attachment.url" target="_blank" rel="noopener noreferrer">
+ <img :src="attachment.url" class="note-image-attach" />
</a>
<div class="attachment">
- <a
- v-if="attachment.url"
- :href="attachment.url"
- target="_blank"
- rel="noopener noreferrer">
- <i
- class="fa fa-paperclip"
- aria-hidden="true">
- </i>
- {{ attachment.filename }}
+ <a v-if="attachment.url" :href="attachment.url" target="_blank" rel="noopener noreferrer">
+ <i class="fa fa-paperclip" aria-hidden="true"> </i> {{ attachment.filename }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 401bcfabbe4..3d60eb02db8 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
awards: {
@@ -167,31 +167,27 @@ export default {
<button
v-for="(awardList, awardName, index) in groupedAwards"
:key="index"
- v-tooltip
+ v-gl-tooltip.bottom="{ boundary: 'viewport' }"
:class="getAwardClassBindings(awardList)"
:title="awardTitle(awardList)"
class="btn award-control"
- data-boundary="viewport"
- data-placement="bottom"
type="button"
- @click="handleAward(awardName)">
+ @click="handleAward(awardName);"
+ >
<span v-html="getAwardHTML(awardName)"></span>
- <span class="award-control-text js-counter">
- {{ awardList.length }}
- </span>
+ <span class="award-control-text js-counter">{{ awardList.length }}</span>
</button>
- <div
- v-if="canAwardEmoji"
- class="award-menu-holder">
+ <div v-if="canAwardEmoji" class="award-menu-holder">
<button
- v-tooltip
+ v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByMe }"
class="award-control btn js-add-award"
title="Add reaction"
aria-label="Add reaction"
data-boundary="viewport"
data-placement="bottom"
- type="button">
+ type="button"
+ >
<span class="award-control-icon award-control-icon-neutral">
<icon name="emoji_slightly_smiling_face" />
</span>
@@ -203,7 +199,8 @@ export default {
</span>
<i
aria-hidden="true"
- class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>
+ class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"
+ ></i>
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 9375627359c..c0bee600181 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -67,13 +67,8 @@ export default {
</script>
<template>
- <div
- ref="note-body"
- :class="{ 'js-task-list-container': canEdit }"
- class="note-body">
- <div
- class="note-text md"
- v-html="note.note_html"></div>
+ <div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body">
+ <div class="note-text md" v-html="note.note_html"></div>
<note-form
v-if="isEditing"
ref="noteForm"
@@ -88,7 +83,8 @@ export default {
v-if="canEdit"
v-model="note.note"
:data-update-url="note.path"
- class="hidden js-task-list-field"></textarea>
+ class="hidden js-task-list-field"
+ ></textarea>
<note-edited-text
v-if="note.last_edited_at"
:edited-at="note.last_edited_at"
@@ -104,9 +100,6 @@ export default {
:toggle-award-path="note.toggle_award_path"
:can-award-emoji="note.current_user.can_award_emoji"
/>
- <note-attachment
- v-if="note.attachment"
- :attachment="note.attachment"
- />
+ <note-attachment v-if="note.attachment" :attachment="note.attachment" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index d848335022f..15ce49d7c31 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -40,16 +40,11 @@ export default {
{{ actionText }}
<template v-if="editedBy">
by
- <a
- :href="editedBy.path"
- class="js-vue-author author-link">
+ <a :href="editedBy.path" :data-user-id="editedBy.id" class="js-user-link author-link">
{{ editedBy.name }}
</a>
</template>
{{ actionDetailText }}
- <time-ago-tooltip
- :time="editedAt"
- tooltip-placement="bottom"
- />
+ <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 31ee8fed984..95164183ccb 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -48,13 +48,19 @@ export default {
required: false,
default: '',
},
+ resolveDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
- isResolving: false,
+ isResolving: this.resolveDiscussion,
+ isUnresolving: !this.resolveDiscussion,
resolveAsThread: true,
};
},
@@ -146,27 +152,14 @@ export default {
</script>
<template>
- <div
- ref="editNoteForm"
- class="note-edit-form current-note-edit-form js-discussion-note-form">
- <div
- v-if="conflictWhileEditing"
- class="js-conflict-edit-warning alert alert-danger">
+ <div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
+ <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a
- :href="noteHash"
- target="_blank"
- rel="noopener noreferrer">
- updated comment
- </a>
- to ensure information is not lost.
+ <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure
+ information is not lost.
</div>
<div class="flash-container timeline-content"></div>
- <form
- :data-line-code="lineCode"
- 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)"
:is-locked="isLocked(getNoteableData)"
@@ -178,7 +171,8 @@ export default {
:markdown-docs-path="markdownDocsPath"
:markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
- :add-spacing-classes="false">
+ :add-spacing-classes="false"
+ >
<textarea
id="note_note"
ref="textarea"
@@ -186,35 +180,36 @@ export default {
v-model="updatedNoteBody"
: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"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
- @keydown.meta.enter="handleUpdate()"
- @keydown.ctrl.enter="handleUpdate()"
- @keydown.up="editMyLastNote()"
- @keydown.esc="cancelHandler(true)">
- </textarea>
+ @keydown.meta.enter="handleUpdate();"
+ @keydown.ctrl.enter="handleUpdate();"
+ @keydown.up="editMyLastNote();"
+ @keydown.esc="cancelHandler(true);"
+ ></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 "
- @click="handleUpdate()">
+ class="js-vue-issue-save btn btn-success js-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)"
+ @click.prevent="handleUpdate(true);"
>
{{ resolveButtonTitle }}
</button>
<button
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
- @click="cancelHandler()">
+ @click="cancelHandler();"
+ >
Cancel
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index dd7313d7b10..7b39901024d 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -63,44 +63,33 @@ export default {
<template>
<div class="note-header-info">
- <div
- v-if="includeToggle"
- class="discussion-actions">
+ <div v-if="includeToggle" class="discussion-actions">
<button
class="note-action-button discussion-toggle-button js-vue-toggle-button"
type="button"
- @click="handleToggle">
- <i
- :class="toggleChevronClass"
- class="fa"
- aria-hidden="true">
- </i>
+ @click="handleToggle"
+ >
+ <i :class="toggleChevronClass" class="fa" aria-hidden="true"> </i>
{{ __('Toggle discussion') }}
</button>
</div>
<a
v-if="hasAuthor"
+ v-once
:href="author.path"
+ class="js-user-link"
+ :data-user-id="author.id"
+ :data-username="author.username"
>
<span class="note-header-author-name">{{ 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 v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
+ <span class="note-headline-light"> @{{ author.username }} </span>
</a>
- <span v-else>
- {{ __('A deleted user') }}
- </span>
+ <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-message"> <slot></slot> </span>
+ <template v-if="createdAt">
<span class="system-note-separator">
<template v-if="actionText">
{{ actionText }}
@@ -109,11 +98,9 @@ export default {
<a
:href="noteTimestampLink"
class="note-timestamp system-note-separator"
- @click="updateTargetNoteHash">
- <time-ago-tooltip
- :time="createdAt"
- tooltip-placement="bottom"
- />
+ @click="updateTargetNoteHash"
+ >
+ <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
</a>
</template>
<i
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 91f7c269757..e3eb92956b1 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -16,10 +16,6 @@ export default {
<template>
<div class="disabled-comment text-center">
- Please
- <a :href="registerLink">register</a>
- or
- <a :href="signInLink">sign in</a>
- to reply
+ Please <a :href="registerLink">register</a> or <a :href="signInLink">sign in</a> to reply
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 7740967ccd5..5c9a28b8512 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,9 +1,12 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
+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';
@@ -20,14 +23,12 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'NoteableDiscussion',
components: {
icon,
noteableNote,
- diffWithNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
@@ -37,9 +38,10 @@ export default {
placeholderNote,
placeholderSystemNote,
systemNote,
+ TimelineEntryItem,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
mixins: [autosave, noteable, resolvable, discussionNavigation],
props: {
@@ -64,43 +66,25 @@ export default {
},
},
data() {
+ const { diff_discussion: isDiffDiscussion, resolved } = this.discussion;
+
return {
isReplying: false,
isResolving: false,
resolveAsThread: true,
- isRepliesCollapsed: (!this.discussion.diff_discussion && this.discussion.resolved) || false,
+ isRepliesCollapsed: Boolean(!isDiffDiscussion && resolved),
};
},
computed: {
...mapGetters([
'getNoteableData',
- 'discussionCount',
- 'resolvedDiscussionCount',
- 'allDiscussions',
- 'unresolvedDiscussionsIdsByDiff',
- 'unresolvedDiscussionsIdsByDate',
- 'unresolvedDiscussions',
- 'unresolvedDiscussionsIdsOrdered',
'nextUnresolvedDiscussionId',
- 'isLastUnresolvedDiscussion',
+ 'unresolvedDiscussionsCount',
+ 'hasUnresolvedDiscussions',
+ 'showJumpToNextDiscussion',
]),
- transformedDiscussion() {
- return {
- ...this.discussion.notes[0],
- truncated_diff_lines: this.discussion.truncated_diff_lines || [],
- truncated_diff_lines_path: this.discussion.truncated_diff_lines_path,
- diff_file: this.discussion.diff_file,
- diff_discussion: this.discussion.diff_discussion,
- active: this.discussion.active,
- discussion_path: this.discussion.discussion_path,
- resolved: this.discussion.resolved,
- resolved_by: this.discussion.resolved_by,
- resolved_by_push: this.discussion.resolved_by_push,
- resolved_at: this.discussion.resolved_at,
- };
- },
author() {
- return this.transformedDiscussion.author;
+ return this.initialDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
@@ -136,29 +120,19 @@ export default {
return null;
},
resolvedText() {
- return this.transformedDiscussion.resolved_by_push ? 'Automatically resolved' : 'Resolved';
+ return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
},
- hasMultipleUnresolvedDiscussions() {
- return this.unresolvedDiscussions.length > 1;
- },
- showJumpToNextDiscussion() {
- return (
- this.hasMultipleUnresolvedDiscussions &&
- !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
+ shouldShowJumpToNextDiscussion() {
+ return this.showJumpToNextDiscussion(
+ this.discussion.id,
+ this.discussionsByDiffOrder ? 'diff' : 'discussion',
);
},
shouldRenderDiffs() {
- return (
- this.transformedDiscussion.diff_discussion &&
- this.transformedDiscussion.diff_file &&
- this.renderDiffFile
- );
+ return this.discussion.diff_discussion && this.renderDiffFile;
},
shouldGroupReplies() {
- return !this.shouldRenderDiffs && !this.transformedDiscussion.diff_discussion;
- },
- shouldRenderHeader() {
- return this.shouldRenderDiffs;
+ return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
@@ -170,9 +144,6 @@ export default {
return {};
},
- wrapperClass() {
- return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
- },
componentClassName() {
if (this.shouldRenderDiffs) {
if (!this.lastUpdatedAt && !this.discussion.resolved) {
@@ -183,11 +154,45 @@ export default {
return '';
},
shouldShowDiscussions() {
- const isExpanded = this.discussion.expanded;
- const { resolved } = this.transformedDiscussion;
- const isResolvedNonDiffDiscussion = !this.transformedDiscussion.diff_discussion && resolved;
+ const { expanded, resolved } = this.discussion;
+ const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved;
+
+ return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ },
+ actionText() {
+ const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
+ const linkEnd = '</a>';
+
+ let { commit_id: commitId } = this.discussion;
+ if (commitId) {
+ commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
+ }
- return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ let text = s__('MergeRequests|started a discussion');
+
+ if (this.discussion.for_commit) {
+ text = s__(
+ 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
+ );
+ } else if (this.discussion.diff_discussion) {
+ if (this.discussion.active) {
+ text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}');
+ } else {
+ text = s__(
+ 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
+ );
+ }
+ }
+
+ return sprintf(
+ text,
+ {
+ commitId,
+ linkStart,
+ linkEnd,
+ },
+ false,
+ );
},
},
watch: {
@@ -195,7 +200,7 @@ export default {
if (this.isReplying) {
this.$nextTick(() => {
// Pass an extra key to separate reply and note edit forms
- this.initAutoSave(this.transformedDiscussion, ['Reply']);
+ this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']);
});
} else {
this.disposeAutoSave();
@@ -302,210 +307,156 @@ Please check your network connection and try again.`;
</script>
<template>
- <li
- class="note note-discussion timeline-entry"
- :class="componentClassName"
- >
- <div class="timeline-entry-inner">
- <div class="timeline-content">
- <div
- :data-discussion-id="transformedDiscussion.discussion_id"
- class="discussion js-discussion-container"
- >
- <div
- v-if="shouldRenderHeader"
- class="discussion-header note-wrapper"
- >
- <div class="timeline-icon">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <note-header
- :author="author"
- :created-at="transformedDiscussion.created_at"
- :note-id="transformedDiscussion.id"
- :include-toggle="true"
- :expanded="discussion.expanded"
- @toggleHandler="toggleDiscussionHandler"
- >
- <template v-if="transformedDiscussion.diff_discussion">
- started a discussion on
- <a :href="transformedDiscussion.discussion_path">
- <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.resolved_at"
- :edited-by="transformedDiscussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline"
- />
- <note-edited-text
- v-else-if="lastUpdatedAt"
- :edited-at="lastUpdatedAt"
- :edited-by="lastUpdatedBy"
- action-text="Last updated"
- class-name="discussion-headline-light js-discussion-headline"
+ <timeline-entry-item class="note note-discussion" :class="componentClassName">
+ <div class="timeline-content">
+ <div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
+ <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
+ <div v-once class="timeline-icon">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
/>
</div>
- <div
- v-if="shouldShowDiscussions"
- class="discussion-body">
- <component
- :is="wrapperComponent"
- v-bind="wrapperComponentProps"
- :class="wrapperClass"
- >
- <div class="discussion-notes">
- <ul class="notes">
- <template v-if="shouldGroupReplies">
- <component
- :is="componentName(initialDiscussion)"
- :note="componentData(initialDiscussion)"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot
- slot="avatar-badge"
- name="avatar-badge"
- >
- </slot>
- </component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="isRepliesCollapsed"
- :replies="replies"
- @toggle="toggleReplies"
- />
- <template v-if="!isRepliesCollapsed">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- />
- </template>
- </template>
- <template v-else>
+ <note-header
+ :author="author"
+ :created-at="initialDiscussion.created_at"
+ :note-id="initialDiscussion.id"
+ :include-toggle="true"
+ :expanded="discussion.expanded"
+ @toggleHandler="toggleDiscussionHandler"
+ >
+ <span v-html="actionText"></span>
+ </note-header>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ <note-edited-text
+ v-else-if="lastUpdatedAt"
+ :edited-at="lastUpdatedAt"
+ :edited-by="lastUpdatedBy"
+ action-text="Last updated"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ </div>
+ <div v-if="shouldShowDiscussions" 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)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="isRepliesCollapsed"
+ :replies="replies"
+ @toggle="toggleReplies"
+ />
+ <template v-if="!isRepliesCollapsed">
<component
:is="componentName(note)"
- v-for="(note, index) in discussion.notes"
+ v-for="note in replies"
:key="note.id"
:note="componentData(note)"
@handleDeleteNote="deleteNoteHandler"
- >
- <slot
- v-if="index === 0"
- slot="avatar-badge"
- name="avatar-badge"
- >
- </slot>
- </component>
+ />
</template>
- </ul>
- <div
- v-if="!isRepliesCollapsed"
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
- >
- <template v-if="!isReplying && canReply">
- <div class="discussion-with-resolve-btn">
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ </template>
+ </ul>
+ <div
+ v-if="!isRepliesCollapsed"
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder"
+ >
+ <template v-if="!isReplying && canReply">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm"
+ class="btn btn-default ml-sm-2"
+ @click="resolveHandler();"
>
- Reply...
+ <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
+ {{ resolveButtonTitle }}
</button>
- <div v-if="discussion.resolvable">
+ </div>
+ <div
+ v-if="discussion.resolvable"
+ class="btn-group discussion-actions ml-sm-2"
+ role="group"
+ >
+ <div v-if="!discussionResolved" class="btn-group" role="group">
+ <a
+ v-gl-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"
+ >
+ <icon name="issue-new" />
+ </a>
+ </div>
+ <div v-if="shouldShowJumpToNextDiscussion" class="btn-group" role="group">
<button
- type="button"
- class="btn btn-default mr-sm-2"
- @click="resolveHandler()"
+ v-gl-tooltip
+ class="btn btn-default discussion-next-btn"
+ title="Jump to next unresolved discussion"
+ @click="jumpToNextDiscussion"
>
- <i
- v-if="isResolving"
- aria-hidden="true"
- class="fa fa-spinner fa-spin"
- ></i>
- {{ resolveButtonTitle }}
+ <icon name="comment-next" />
</button>
</div>
- <div
- v-if="discussion.resolvable"
- class="btn-group discussion-actions ml-sm-2"
- role="group"
- >
- <div
- v-if="!discussionResolved"
- class="btn-group"
- role="group">
- <a
- 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"
- data-container="body"
- >
- <icon name="issue-new" />
- </a>
- </div>
- <div
- v-if="showJumpToNextDiscussion"
- class="btn-group"
- role="group">
- <button
- v-tooltip
- class="btn btn-default discussion-next-btn"
- title="Jump to next unresolved discussion"
- data-container="body"
- @click="jumpToNextDiscussion"
- >
- <icon name="comment-next" />
- </button>
- </div>
- </div>
</div>
- </template>
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- save-button-title="Comment"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- <note-signed-out-widget v-if="!canReply" />
- </div>
+ </div>
+ </template>
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
+ save-button-title="Comment"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ <note-signed-out-widget v-if="!canReply" />
</div>
- </component>
- </div>
+ </div>
+ </component>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9ab91e2abe5..a17be51353e 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,6 +2,7 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -18,6 +19,7 @@ export default {
noteHeader,
noteActions,
noteBody,
+ TimelineEntryItem,
},
mixins: [noteable, resolvable],
props: {
@@ -169,65 +171,60 @@ export default {
</script>
<template>
- <li
+ <timeline-entry-item
:id="noteAnchorId"
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry note-wrapper"
+ class="note note-wrapper"
>
- <div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- >
- <slot
- slot="avatar-badge"
- name="avatar-badge"
- >
- </slot>
- </user-avatar-link>
- </div>
- <div class="timeline-content">
- <div class="note-header">
- <note-header
- :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"
- :is-resolving="isResolving"
- :resolved-by="note.resolved_by"
- @handleEdit="editHandler"
- @handleDelete="deleteHandler"
- @handleResolve="resolveHandler"
- />
- </div>
- <note-body
- ref="noteBody"
- :note="note"
+ <div v-once class="timeline-icon">
+ <user-avatar-link
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"> </slot>
+ </user-avatar-link>
+ </div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header
+ v-once
+ :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"
- :is-editing="isEditing"
- @handleFormUpdate="formUpdateHandler"
- @cancelForm="formCancelHandler"
+ :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"
+ :is-resolving="isResolving"
+ :resolved-by="note.resolved_by"
+ @handleEdit="editHandler"
+ @handleDelete="deleteHandler"
+ @handleResolve="resolveHandler"
/>
</div>
+ <note-body
+ ref="noteBody"
+ :note="note"
+ :can-edit="note.current_user.can_edit"
+ :is-editing="isEditing"
+ @handleFormUpdate="formUpdateHandler"
+ @cancelForm="formCancelHandler"
+ />
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 69ddfd751e0..445d3267a3f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -12,6 +12,7 @@ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import initUserPopovers from '../../user_popovers';
export default {
name: 'NotesApp',
@@ -22,6 +23,7 @@ export default {
commentForm,
placeholderNote,
placeholderSystemNote,
+ skeletonLoadingContainer,
},
props: {
noteableData: {
@@ -59,7 +61,6 @@ export default {
'isNotesFetched',
'discussions',
'getNotesDataByProp',
- 'discussionCount',
'isLoading',
'commentsDisabled',
]),
@@ -106,42 +107,28 @@ export default {
}
},
updated() {
- this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')));
+ this.$nextTick(() => {
+ highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
+ initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
+ });
},
methods: {
- ...mapActions({
- setLoadingState: 'setLoadingState',
- fetchDiscussions: 'fetchDiscussions',
- poll: 'poll',
- actionToggleAward: 'toggleAward',
- scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
- setNotesData: 'setNotesData',
- setNoteableData: 'setNoteableData',
- setUserData: 'setUserData',
- setLastFetchedAt: 'setLastFetchedAt',
- setTargetNoteHash: 'setTargetNoteHash',
- toggleDiscussion: 'toggleDiscussion',
- setNotesFetchedState: 'setNotesFetchedState',
- startTaskList: 'startTaskList',
- }),
- getComponentName(discussion) {
- if (discussion.isSkeletonNote) {
- return skeletonLoadingContainer;
- }
- if (discussion.isPlaceholderNote) {
- if (discussion.placeholderType === constants.SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
- return placeholderNote;
- } else if (discussion.individual_note) {
- return discussion.notes[0].system ? systemNote : noteableNote;
- }
-
- return noteableDiscussion;
- },
- getComponentData(discussion) {
- return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
- },
+ ...mapActions([
+ 'setLoadingState',
+ 'fetchDiscussions',
+ 'poll',
+ 'toggleAward',
+ 'scrollToNoteIfNeeded',
+ 'setNotesData',
+ 'setNoteableData',
+ 'setUserData',
+ 'setLastFetchedAt',
+ 'setTargetNoteHash',
+ 'toggleDiscussion',
+ 'setNotesFetchedState',
+ 'expandDiscussion',
+ 'startTaskList',
+ ]),
fetchNotes() {
if (this.isFetching) return null;
@@ -181,37 +168,46 @@ export default {
const noteId = hash && hash.replace(/^note_/, '');
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 });
- }
- });
- }
- });
+ const discussion = this.discussions.find(d => d.notes.some(({ id }) => id === noteId));
+
+ if (discussion) {
+ this.expandDiscussion({ discussionId: discussion.id });
+ }
}
},
},
+ systemNote: constants.SYSTEM_NOTE,
};
</script>
<template>
- <div
- v-show="shouldShow"
- id="notes"
- >
- <ul
- id="notes-list"
- class="notes main-notes-list timeline"
- >
- <component
- :is="getComponentName(discussion)"
- v-for="discussion in allDiscussions"
- :key="discussion.id"
- v-bind="getComponentData(discussion)"
- />
+ <div v-show="shouldShow" id="notes">
+ <ul id="notes-list" class="notes main-notes-list timeline">
+ <template v-for="discussion in allDiscussions">
+ <skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" />
+ <template v-else-if="discussion.isPlaceholderNote">
+ <placeholder-system-note
+ v-if="discussion.placeholderType === $options.systemNote"
+ :key="discussion.id"
+ :note="discussion.notes[0]"
+ />
+ <placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" />
+ </template>
+ <template v-else-if="discussion.individual_note">
+ <system-note
+ v-if="discussion.notes[0].system"
+ :key="discussion.id"
+ :note="discussion.notes[0]"
+ />
+ <noteable-note v-else :key="discussion.id" :note="discussion.notes[0]" />
+ </template>
+ <noteable-discussion
+ v-else
+ :key="discussion.id"
+ :discussion="discussion"
+ :render-diff-file="true"
+ />
+ </template>
</ul>
<comment-form
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 78ecbbb9247..72a8ff28466 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -42,15 +42,9 @@ export default {
</script>
<template>
- <li
- :class="className"
- class="replies-toggle"
- >
+ <li :class="className" class="replies-toggle js-toggle-replies">
<template v-if="collapsed">
- <icon
- name="chevron-right"
- @click.native="toggle"
- />
+ <icon name="chevron-right" @click.native="toggle" />
<div>
<user-avatar-link
v-for="author in uniqueAuthors"
@@ -63,32 +57,17 @@ export default {
tooltip-placement="bottom"
/>
</div>
- <button
- class="btn btn-link js-replies-text"
- type="button"
- @click="toggle"
- >
+ <button class="btn btn-link js-replies-text" type="button" @click="toggle">
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</button>
{{ __('Last reply by') }}
- <a
- :href="lastReply.author.path"
- class="btn btn-link author-link"
- >
+ <a :href="lastReply.author.path" class="btn btn-link author-link">
{{ lastReply.author.name }}
</a>
- <time-ago-tooltip
- :time="lastReply.created_at"
- tooltip-placement="bottom"
- />
+ <time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" />
</template>
- <span
- v-else
- class="collapse-replies-btn js-collapse-replies"
- @click="toggle"
- >
- <icon name="chevron-down" />
- {{ s__('Notes|Collapse replies') }}
+ <span v-else class="collapse-replies-btn js-collapse-replies" @click="toggle">
+ <icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
</span>
</li>
</template>
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index f7c4deee1f8..3d89d907777 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,29 +1,56 @@
import { scrollToElement } from '~/lib/utils/common_utils';
+import eventHub from '../../notes/event_hub';
export default {
methods: {
- jumpToDiscussion(id) {
- if (id) {
- const activeTab = window.mrTabs.currentAction;
- const selector =
- activeTab === 'diffs'
- ? `ul.notes[data-discussion-id="${id}"]`
- : `div.discussion[data-discussion-id="${id}"]`;
- const el = document.querySelector(selector);
+ diffsJump(id) {
+ const selector = `ul.notes[data-discussion-id="${id}"]`;
- if (activeTab === 'commits' || activeTab === 'pipelines') {
- window.mrTabs.activateTab('show');
- }
+ eventHub.$once('scrollToDiscussion', () => {
+ const el = document.querySelector(selector);
if (el) {
- this.expandDiscussion({ discussionId: id });
-
scrollToElement(el);
+
return true;
}
+
+ return false;
+ });
+
+ this.expandDiscussion({ discussionId: id });
+ },
+ discussionJump(id) {
+ const selector = `div.discussion[data-discussion-id="${id}"]`;
+
+ const el = document.querySelector(selector);
+
+ this.expandDiscussion({ discussionId: id });
+
+ if (el) {
+ scrollToElement(el);
+
+ return true;
}
return false;
},
+ jumpToDiscussion(id) {
+ if (id) {
+ const activeTab = window.mrTabs.currentAction;
+
+ if (activeTab === 'diffs') {
+ this.diffsJump(id);
+ } else if (activeTab === 'commits' || activeTab === 'pipelines') {
+ window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
+ setTimeout(() => this.discussionJump(id), 0);
+ });
+
+ window.mrTabs.tabShown('show');
+ } else {
+ this.discussionJump(id);
+ }
+ }
+ },
},
};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index cd8394e0619..8edf3d088bb 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -36,7 +36,7 @@ export default {
const discussion = this.resolveAsThread;
const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
- this.toggleResolveNote({ endpoint, isResolved, discussion })
+ return this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
this.isResolving = false;
})
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 5b2f0540020..4716ab52333 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -11,13 +11,19 @@ import * as constants from '../constants';
import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
-import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
+import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import { __ } from '~/locale';
let eTagPoll;
-export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
+export const expandDiscussion = ({ commit, dispatch }, data) => {
+ if (data.discussionId) {
+ dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true });
+ }
+
+ commit(types.EXPAND_DISCUSSION, data);
+};
export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data);
@@ -39,12 +45,13 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchDiscussions = ({ commit }, { path, filter }) =>
+export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
service
.fetchDiscussions(path, filter)
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+ dispatch('updateResolvableDiscussonsCounts');
});
export const updateDiscussion = ({ commit, state }, discussion) => {
@@ -53,11 +60,18 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
return utils.findNoteObjectById(state.discussions, discussion.id);
};
-export const deleteNote = ({ commit, dispatch }, note) =>
+export const deleteNote = ({ commit, dispatch, state }, note) =>
service.deleteNote(note.path).then(() => {
+ const discussion = state.discussions.find(({ id }) => id === note.discussion_id);
+
commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget');
+ dispatch('updateResolvableDiscussonsCounts');
+
+ if (isInMRPage()) {
+ dispatch('diffs/removeDiscussionsFromDiff', discussion);
+ }
});
export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
@@ -89,6 +103,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
+ dispatch('updateResolvableDiscussonsCounts');
}
return res;
});
@@ -104,6 +119,8 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
commit(mutationType, res);
+ dispatch('updateResolvableDiscussonsCounts');
+
dispatch('updateMergeRequestWidget');
});
@@ -385,5 +402,8 @@ export const startTaskList = ({ dispatch }) =>
}),
);
+export const updateResolvableDiscussonsCounts = ({ commit }) =>
+ commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 8df95c279eb..0ffc0cb2593 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -53,43 +53,41 @@ export const getCurrentUserLastNote = state =>
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
-export const discussionCount = state => {
- const filteredDiscussions = state.discussions.filter(n => !n.individual_note && n.resolvable);
+export const unresolvedDiscussionsCount = state => state.unresolvedDiscussionsCount;
+export const resolvableDiscussionsCount = state => state.resolvableDiscussionsCount;
+export const hasUnresolvedDiscussions = state => state.hasUnresolvedDiscussions;
- return filteredDiscussions.length;
-};
-
-export const unresolvedDiscussions = (state, getters) => {
- const resolvedMap = getters.resolvedDiscussionsById;
-
- return state.discussions.filter(n => !n.individual_note && !resolvedMap[n.id]);
-};
+export const showJumpToNextDiscussion = (state, getters) => (discussionId, mode = 'discussion') => {
+ const orderedDiffs =
+ mode !== 'discussion'
+ ? getters.unresolvedDiscussionsIdsByDiff
+ : getters.unresolvedDiscussionsIdsByDate;
-export const allDiscussions = (state, getters) => {
- const resolved = getters.resolvedDiscussionsById;
- const unresolved = getters.unresolvedDiscussions;
+ const indexOf = orderedDiffs.indexOf(discussionId);
- return Object.values(resolved).concat(unresolved);
+ return indexOf !== -1 && indexOf < orderedDiffs.length - 1;
};
export const isDiscussionResolved = (state, getters) => discussionId =>
getters.resolvedDiscussionsById[discussionId] !== undefined;
-export const allResolvableDiscussions = (state, getters) =>
- getters.allDiscussions.filter(d => !d.individual_note && d.resolvable);
+export const allResolvableDiscussions = state =>
+ state.discussions.filter(d => !d.individual_note && d.resolvable);
export const resolvedDiscussionsById = state => {
const map = {};
- state.discussions.filter(d => d.resolvable).forEach(n => {
- if (n.notes) {
- const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved);
+ state.discussions
+ .filter(d => d.resolvable)
+ .forEach(n => {
+ if (n.notes) {
+ const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved);
- if (resolved) {
- map[n.id] = n;
+ if (resolved) {
+ map[n.id] = n;
+ }
}
- }
- });
+ });
return map;
};
@@ -117,7 +115,7 @@ export const unresolvedDiscussionsIdsByDate = (state, getters) =>
// line numbers.
export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
getters.allResolvableDiscussions
- .filter(d => !d.resolved)
+ .filter(d => !d.resolved && d.active)
.sort((a, b) => {
if (!a.diff_file || !b.diff_file) {
return 0;
@@ -145,15 +143,12 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
-export const discussionTabCounter = state => {
- let all = [];
-
- state.discussions.forEach(discussion => {
- all = all.concat(discussion.notes.filter(note => !note.system && !note.placeholder));
- });
-
- return all.length;
-};
+export const discussionTabCounter = state =>
+ state.discussions.reduce(
+ (acc, discussion) =>
+ acc + discussion.notes.filter(note => !note.system && !note.placeholder).length,
+ 0,
+ );
// Returns the list of discussion IDs ordered according to given parameter
// @param {Boolean} diffOrder - is ordered by diff?
@@ -180,8 +175,10 @@ export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, dif
export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
const currentIndex = idsOrdered.indexOf(discussionId);
+ const slicedIds = idsOrdered.slice(currentIndex + 1, currentIndex + 2);
- return idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0];
+ // Get the first ID if there is none after the currentIndex
+ return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
};
// @param {Boolean} diffOrder - is ordered by diff?
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 8aea269ea7d..b5fe8bdb1d3 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -22,6 +22,9 @@ export default () => ({
current_user: {},
},
commentsDisabled: false,
+ resolvableDiscussionsCount: 0,
+ unresolvedDiscussionsCount: 0,
+ hasUnresolvedDiscussions: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index dfbf3b7b34b..9c68ab67a8c 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -21,6 +21,7 @@ export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
+export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index f6054e0be87..39ff0ff73d7 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -22,8 +22,10 @@ export default {
if (isDiscussion && isInMRPage()) {
noteData.resolvable = note.resolvable;
noteData.resolved = false;
+ noteData.active = true;
noteData.resolve_path = note.resolve_path;
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
+ noteData.diff_discussion = false;
}
state.discussions.push(noteData);
@@ -97,33 +99,36 @@ export default {
},
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
- const discussions = [];
+ const discussions = discussionsData.reduce((acc, d) => {
+ const discussion = { ...d };
+ const diffData = {};
- discussionsData.forEach(discussion => {
if (discussion.diff_file) {
- Object.assign(discussion, {
- file_hash: discussion.diff_file.file_hash,
- truncated_diff_lines: discussion.truncated_diff_lines || [],
- });
+ diffData.file_hash = discussion.diff_file.file_hash;
+ diffData.truncated_diff_lines = discussion.truncated_diff_lines || [];
}
// To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach(n => {
- discussions.push({
+ acc.push({
...discussion,
+ ...diffData,
notes: [n], // override notes array to only have one item to mimick individual_note
});
});
} else {
const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
- discussions.push({
+ acc.push({
...discussion,
+ ...diffData,
expanded: oldNote ? oldNote.expanded : discussion.expanded,
});
}
- });
+
+ return acc;
+ }, []);
Object.assign(state, { discussions });
},
@@ -174,9 +179,11 @@ export default {
}
},
- [types.TOGGLE_DISCUSSION](state, { discussionId }) {
+ [types.TOGGLE_DISCUSSION](state, { discussionId, forceExpanded = null }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- Object.assign(discussion, { expanded: !discussion.expanded });
+ Object.assign(discussion, {
+ expanded: forceExpanded === null ? !discussion.expanded : forceExpanded,
+ });
},
[types.UPDATE_NOTE](state, note) {
@@ -195,7 +202,9 @@ export default {
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
if (note.diff_file) {
- Object.assign(note, { file_hash: note.diff_file.file_hash });
+ Object.assign(note, {
+ file_hash: note.diff_file.file_hash,
+ });
}
Object.assign(selectedDiscussion, { ...note });
},
@@ -229,4 +238,16 @@ export default {
[types.DISABLE_COMMENTS](state, value) {
state.commentsDisabled = value;
},
+ [types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS](state) {
+ state.resolvableDiscussionsCount = state.discussions.filter(
+ discussion => !discussion.individual_note && discussion.resolvable,
+ ).length;
+ state.unresolvedDiscussionsCount = state.discussions.filter(
+ discussion =>
+ !discussion.individual_note &&
+ discussion.resolvable &&
+ discussion.notes.some(note => note.resolvable && !note.resolved),
+ ).length;
+ state.hasUnresolvedDiscussions = state.unresolvedDiscussionsCount > 1;
+ },
};