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.vue9
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue19
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue67
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue19
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue38
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue24
-rw-r--r--app/assets/javascripts/notes/components/timeline_toggle.vue1
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue4
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js34
-rw-r--r--app/assets/javascripts/notes/i18n.js2
-rw-r--r--app/assets/javascripts/notes/index.js13
-rw-r--r--app/assets/javascripts/notes/mixins/diff_line_note_form.js6
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js156
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js4
-rw-r--r--app/assets/javascripts/notes/timeline.js16
-rw-r--r--app/assets/javascripts/notes/utils/get_notes_filter_data.js21
23 files changed, 158 insertions, 301 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index bf35d5c3b25..0d7ff022f8f 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -5,7 +5,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
@@ -111,7 +111,10 @@ export default {
return this.getNoteableData.current_user.can_create_note;
},
canSetInternalNote() {
- return this.getNoteableData.current_user.can_update && (this.isIssue || this.isEpic);
+ return (
+ this.getNoteableData.current_user.can_create_confidential_note &&
+ (this.isIssue || this.isEpic)
+ );
},
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
@@ -276,7 +279,7 @@ export default {
.then(() => badgeState.updateStatus && badgeState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() =>
- createFlash({
+ createAlert({
message: constants.toggleStateErrorMessage[this.noteableType][this.openState],
}),
);
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 1b1923a90f7..cf6474270a2 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -84,8 +84,8 @@ export default {
return sprintf(text, { commitDisplay, linkStart, linkEnd }, false);
},
- adaptiveAvatarSize() {
- return { default: 24, md: 32 };
+ toggleClass() {
+ return this.discussion.expanded ? 'expanded' : 'collapsed';
},
},
methods: {
@@ -98,16 +98,13 @@ export default {
</script>
<template>
- <div class="discussion-header gl-display-flex gl-align-items-center gl-p-5">
- <div
- v-once
- class="timeline-icon gl-align-self-start gl-flex-shrink-0 gl-flex-shrink gl-mx-3 gl-md-ml-2 gl-md-mr-5"
- >
+ <div class="discussion-header gl-display-flex gl-align-items-center">
+ <div v-once class="timeline-avatar gl-align-self-start gl-flex-shrink-0 gl-flex-shrink">
<gl-avatar-link v-if="author" :href="author.path">
- <gl-avatar :src="author.avatar_url" :alt="author.name" :size="adaptiveAvatarSize" />
+ <gl-avatar :src="author.avatar_url" :alt="author.name" :size="32" />
</gl-avatar-link>
</div>
- <div class="timeline-content w-100">
+ <div class="timeline-content w-100 gl-ml-3" :class="toggleClass">
<note-header
:author="author"
:created-at="firstNote.created_at"
@@ -123,14 +120,14 @@ export default {
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline gl-pl-2"
+ class-name="discussion-headline-light js-discussion-headline gl-pl-3"
/>
<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 gl-pl-2"
+ class-name="discussion-headline-light js-discussion-headline gl-pl-3"
/>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 6521b86edbb..37935e9c3c6 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -81,16 +81,18 @@ export default {
:class="{
'gl-bg-orange-50': blocksMerge && !allResolved,
'gl-bg-gray-50': !blocksMerge || allResolved,
- 'gl-pr-2': !allResolved,
}"
data-testid="discussions-counter-text"
>
<template v-if="allResolved">
{{ __('All threads resolved!') }}
<gl-dropdown
+ v-gl-tooltip:discussionCounter.hover.bottom
size="small"
category="tertiary"
right
+ :title="__('Thread options')"
+ :aria-label="__('Thread options')"
toggle-class="btn-icon"
class="gl-pt-0! gl-px-2 gl-h-full gl-ml-2"
>
@@ -133,9 +135,12 @@ export default {
@click="jumpNext"
/>
<gl-dropdown
+ v-gl-tooltip:discussionCounter.hover.bottom
size="small"
category="tertiary"
right
+ :title="__('Thread options')"
+ :aria-label="__('Thread options')"
toggle-class="btn-icon"
class="gl-pt-0! gl-px-2"
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 8a42fb6bd85..21b48a2a666 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -168,7 +168,7 @@ export default {
id="discussion-preferences-dropdown"
class="full-width-mobile"
data-qa-selector="discussion_preferences_dropdown"
- text="Sort or filter"
+ :text="__('Sort or filter')"
:disabled="isLoading"
right
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 61af0b06535..39b3df899a5 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -31,7 +31,7 @@ export default {
<div class="timeline-icon d-none d-lg-flex">
<gl-icon name="comment" />
</div>
- <div class="timeline-content">
+ <div class="timeline-content gl-pl-8">
<div data-testid="discussion-filter-timeline-content">
<gl-sprintf :message="$options.i18n.information">
<template #bold="{ content }">
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 6fcfa66ea49..2dbc9b10836 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -142,7 +142,7 @@ export default {
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text gl-mb-2 gl-ml-3"
/>
</template>
<template #avatar-badge>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 9806f8e5dc2..930876e90b1 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui
import { mapActions, mapGetters, mapState } from 'vuex';
import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -238,7 +238,7 @@ export default {
})
.then(() => this.handleAssigneeUpdate(assignees))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong while updating assignees'),
}),
);
@@ -281,6 +281,7 @@ export default {
>
{{ __('Contributor') }}
</user-access-role-badge>
+ <span class="note-actions__mobile-spacer"></span>
<gl-button
v-if="canResolve"
ref="resolveButton"
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 835750cc137..9d59994788e 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import AwardsList from '~/vue_shared/components/awards_list.vue';
@@ -49,7 +49,7 @@ export default {
};
this.toggleAwardRequest(data).catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
}),
);
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index f700802d6bc..f3530344181 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -9,8 +9,6 @@ import {
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
@@ -21,13 +19,11 @@ export default {
GlIcon,
GlBadge,
GlLoadingIcon,
- UserNameWithStatus,
},
directives: {
SafeHtml,
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
author: {
type: Object,
@@ -74,12 +70,15 @@ export default {
required: false,
default: false,
},
+ isSystemNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
isUsernameLinkHovered: false,
- emojiTitle: '',
- authorStatusHasTooltip: false,
};
},
computed: {
@@ -100,15 +99,6 @@ export default {
'js-user-link': true,
};
},
- authorStatus() {
- if (this.author?.show_status) {
- return this.author.status_tooltip_html;
- }
- return false;
- },
- emojiElement() {
- return this.$refs?.authorStatus?.querySelector('gl-emoji');
- },
authorName() {
return this.author.name;
},
@@ -116,14 +106,6 @@ export default {
return s__('Notes|This internal note will always remain confidential');
},
},
- mounted() {
- this.emojiTitle = this.emojiElement ? this.emojiElement.getAttribute('title') : '';
-
- const authorStatusTitle = this.$refs?.authorStatus
- ?.querySelector('.user-status-emoji')
- ?.getAttribute('title');
- this.authorStatusHasTooltip = authorStatusTitle && authorStatusTitle !== '';
- },
methods: {
...mapActions(['setTargetNoteHash']),
handleToggle() {
@@ -134,12 +116,6 @@ export default {
this.setTargetNoteHash(this.noteTimestampLink);
}
},
- removeEmojiTitle() {
- this.emojiElement.removeAttribute('title');
- },
- addEmojiTitle() {
- this.emojiElement.setAttribute('title', this.emojiTitle);
- },
handleUsernameMouseEnter() {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseenter'));
this.isUsernameLinkHovered = true;
@@ -148,9 +124,6 @@ export default {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave'));
this.isUsernameLinkHovered = false;
},
- userAvailability(selectedAuthor) {
- return selectedAuthor?.availability || '';
- },
},
i18n: {
showThread: __('Show thread'),
@@ -185,35 +158,11 @@ export default {
:data-user-id="author.id"
:data-username="author.username"
>
- <span
- v-if="glFeatures.removeUserAttributesProjects || glFeatures.removeUserAttributesGroups"
- class="note-header-author-name gl-font-weight-bold"
- >
+ <span class="note-header-author-name gl-font-weight-bold">
{{ authorName }}
</span>
- <user-name-with-status
- v-else
- :name="authorName"
- :availability="userAvailability(author)"
- container-classes="note-header-author-name gl-font-weight-bold"
- />
</a>
- <span
- v-if="
- authorStatus &&
- !glFeatures.removeUserAttributesProjects &&
- !glFeatures.removeUserAttributesGroups
- "
- ref="authorStatus"
- v-safe-html:[$options.safeHtmlConfig]="authorStatus"
- v-on="
- authorStatusHasTooltip ? { mouseenter: removeEmojiTitle, mouseleave: addEmojiTitle } : {}
- "
- ></span>
- <span
- v-if="!glFeatures.removeUserAttributesProjects && !glFeatures.removeUserAttributesGroups"
- class="text-nowrap author-username"
- >
+ <span v-if="!isSystemNote" class="text-nowrap author-username">
<a
ref="authorUsernameLink"
class="author-username-link"
@@ -252,7 +201,7 @@ export default {
data-testid="internalNoteIndicator"
variant="warning"
size="sm"
- class="gl-mb-3 gl-ml-2"
+ class="gl-ml-2"
:title="internalNoteTooltip"
>
{{ __('Internal note') }}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index afa5e39d8b0..50d166b6db5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -247,7 +247,7 @@ export default {
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
);
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index e51969f95c7..c4b3111b919 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -5,7 +5,7 @@ import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -199,9 +199,6 @@ export default {
isMRDiffView() {
return this.line && !this.isOverviewTab;
},
- authorAvatarAdaptiveSize() {
- return { default: 24, md: 32 };
- },
},
created() {
const line = this.note.position?.line_range?.start || this.line;
@@ -273,7 +270,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting your note. Please try again.'),
});
this.isDeleting = false;
@@ -352,7 +349,7 @@ export default {
},
handleUpdateError() {
const msg = __('Something went wrong while editing your comment. Please try again.');
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
@@ -409,13 +406,13 @@ export default {
:class="{ ...classNameBindings, 'internal-note': note.internal }"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note note-wrapper"
+ class="note note-wrapper note-comment"
data-qa-selector="noteable_note_container"
>
<div
v-if="showMultiLineComment"
data-testid="multiline-comment"
- class="gl-mb-5 gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-4"
+ class="gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-px-5 gl-py-3"
>
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>
@@ -427,7 +424,7 @@ export default {
</gl-sprintf>
</div>
- <div v-if="isMRDiffView" class="gl-float-left gl-mt-n1 gl-mr-3">
+ <div v-if="isMRDiffView" class="timeline-avatar gl-float-left gl-pt-2">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
@@ -440,13 +437,13 @@ export default {
</gl-avatar-link>
</div>
- <div v-else class="gl-float-left gl-pl-3 gl-md-pl-2">
+ <div v-else class="timeline-avatar gl-float-left">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
:entity-name="author.username"
:alt="author.name"
- :size="authorAvatarAdaptiveSize"
+ :size="32"
/>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
new file mode 100644
index 00000000000..e4f88962731
--- /dev/null
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -0,0 +1,38 @@
+<script>
+import DiscussionFilter from './discussion_filter.vue';
+
+export default {
+ components: {
+ TimelineToggle: () => import('./timeline_toggle.vue'),
+ DiscussionFilter,
+ },
+ inject: {
+ showTimelineViewToggle: {
+ default: false,
+ },
+ },
+ props: {
+ notesFilters: {
+ type: Array,
+ required: true,
+ },
+ notesFilterValue: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-sm-align-items-center gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-pt-5 gl-mt-5 gl-border-t"
+ >
+ <h2 class="gl-font-size-h1 gl-m-0">{{ __('Activity') }}</h2>
+ <div class="gl-display-flex gl-gap-3 gl-w-full gl-sm-w-auto gl-mt-3 gl-sm-mt-0">
+ <timeline-toggle v-if="showTimelineViewToggle" />
+ <discussion-filter :filters="notesFilters" :selected-value="notesFilterValue" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 37bc8bad305..9c2ff2c3e7f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,7 +1,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@@ -19,10 +19,12 @@ import DiscussionFilterNote from './discussion_filter_note.vue';
import NoteableDiscussion from './noteable_discussion.vue';
import NoteableNote from './noteable_note.vue';
import SidebarSubscription from './sidebar_subscription.vue';
+import NotesActivityHeader from './notes_activity_header.vue';
export default {
name: 'NotesApp',
components: {
+ NotesActivityHeader,
NoteableNote,
NoteableDiscussion,
SystemNote,
@@ -46,6 +48,15 @@ export default {
type: Object,
required: true,
},
+ notesFilters: {
+ type: Array,
+ required: true,
+ },
+ notesFilterValue: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
userData: {
type: Object,
required: false,
@@ -221,7 +232,7 @@ export default {
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching comments. Please try again.'),
});
});
@@ -281,6 +292,7 @@ export default {
<template>
<div v-show="shouldShow" id="notes">
<sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" />
+ <notes-activity-header :notes-filters="notesFilters" :notes-filter-value="notesFilterValue" />
<ordered-layout :slot-keys="slotKeys">
<template #form>
<comment-form
@@ -292,7 +304,11 @@ export default {
<template #comments>
<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" />
+ <skeleton-loading-container
+ v-if="discussion.isSkeletonNote"
+ :key="discussion.id"
+ class="note-skeleton"
+ />
<timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id">
<draft-note :draft="discussion" />
</timeline-entry-item>
@@ -327,7 +343,7 @@ export default {
:help-page-path="helpPagePath"
/>
</template>
- <discussion-filter-note v-show="commentsDisabled" />
+ <discussion-filter-note v-if="commentsDisabled" />
</ul>
</template>
</ordered-layout>
diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue
index 8632eea5d8e..59a3cc2d306 100644
--- a/app/assets/javascripts/notes/components/timeline_toggle.vue
+++ b/app/assets/javascripts/notes/components/timeline_toggle.vue
@@ -53,6 +53,7 @@ export default {
:selected="timelineEnabled"
:title="tooltip"
:aria-label="tooltip"
+ data-testid="timeline-toggle-button"
@click="toggleTimeline"
/>
</template>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 2bd3488ae1b..734e08dd586 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -61,7 +61,7 @@ export default {
<template>
<li
:class="liClasses"
- class="gl-display-flex! gl-align-items-center gl-flex-wrap gl-bg-gray-10 gl-py-3 gl-px-5 gl-border-t"
+ class="toggle-replies-widget gl-display-flex! gl-align-items-center gl-flex-wrap gl-bg-gray-10 gl-py-3 gl-px-5 gl-border"
>
<gl-button
ref="toggle"
@@ -75,7 +75,7 @@ export default {
<user-avatar-link
v-for="author in uniqueAuthors"
:key="author.username"
- class="gl-mr-3"
+ class="gl-mr-3 reply-author-avatar"
:link-href="author.path"
:img-alt="author.name"
img-css-classes="gl-mr-0!"
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
deleted file mode 100644
index 104e9d4183a..00000000000
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import Vue from 'vue';
-import DiscussionFilter from './components/discussion_filter.vue';
-
-export default (store) => {
- const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
-
- if (discussionFilterEl) {
- const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
- const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
- const filters = Object.keys(filterValues).map((entry) => ({
- title: entry,
- value: filterValues[entry],
- }));
- const props = { filters };
-
- if (defaultFilter) {
- props.selectedValue = parseInt(defaultFilter, 10);
- }
-
- return new Vue({
- el: discussionFilterEl,
- name: 'DiscussionFilterRoot',
- components: {
- DiscussionFilter,
- },
- store,
- render(createElement) {
- return createElement('discussion-filter', { props });
- },
- });
- }
-
- return null;
-};
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 08792fd1a3f..9b5fd69f816 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -16,7 +16,7 @@ export const COMMENT_FORM = {
bodyPlaceholderInternal: __('Write an internal note or drag your files hereā€¦'),
internal: s__('Notes|Make this an internal note'),
internalVisibility: s__(
- 'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
+ 'Notes|Internal notes are only visible to members with the role of Reporter or higher',
),
discussionThatNeedsResolution: __(
'Discuss a specific suggestion or question that needs to be resolved.',
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 054a5bd36e2..defcb0533b7 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotesApp from './components/notes_app.vue';
-import initDiscussionFilters from './discussion_filters';
import { store } from './stores';
-import initTimelineToggle from './timeline';
+import { getNotesFilterData } from './utils/get_notes_filter_data';
export default () => {
const el = document.getElementById('js-vue-notes');
@@ -11,6 +10,9 @@ export default () => {
return;
}
+ const notesFilterProps = getNotesFilterData(el);
+ const showTimelineViewToggle = parseBoolean(el.dataset.showTimelineViewToggle);
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -19,6 +21,9 @@ export default () => {
NotesApp,
},
store,
+ provide: {
+ showTimelineViewToggle,
+ },
data() {
const notesDataset = el.dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
@@ -56,11 +61,9 @@ export default () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ ...notesFilterProps,
},
});
},
});
-
- initDiscussionFilters(store);
- initTimelineToggle(store);
};
diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
index 7b9c0959464..9a140029c07 100644
--- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js
+++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
@@ -1,7 +1,7 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils';
import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { clearDraft } from '~/lib/utils/autosave';
import { s__ } from '~/locale';
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
@@ -42,7 +42,7 @@ export default {
this.handleClearForm(this.discussion.line_code);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
@@ -82,7 +82,7 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index db5f9ebf3f0..d75a4158440 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,136 +1,12 @@
import { mapGetters, mapActions, mapState } from 'vuex';
-import { scrollToElementWithContext, scrollToElement, contentTop } from '~/lib/utils/common_utils';
-import { updateHistory } from '~/lib/utils/url_utility';
-import eventHub from '../event_hub';
-
-/**
- * @param {string} selector
- * @returns {boolean}
- */
-function scrollTo(selector, { withoutContext = false, offset = 0 } = {}) {
- const el = document.querySelector(selector);
- const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
-
- if (el) {
- scrollFunction(el, {
- behavior: 'auto',
- offset,
- });
- return true;
- }
-
- return false;
-}
-
-function updateUrlWithNoteId(noteId) {
- const newHistoryEntry = {
- state: null,
- title: window.title,
- url: `#note_${noteId}`,
- replace: true,
- };
-
- if (noteId) {
- // Temporarily mask the ID to avoid the browser default
- // scrolling taking over which is broken with virtual
- // scrolling enabled.
- const note = document.querySelector(`#note_${noteId}`);
- note?.setAttribute('id', `masked::${note.id}`);
-
- // Update the hash now that the ID "doesn't exist" in the page
- updateHistory(newHistoryEntry);
-
- // Unmask the note's ID
- note?.setAttribute('id', `note_${noteId}`);
- }
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- */
-function diffsJump({ expandDiscussion }, id, firstNoteId) {
- const selector = `ul.notes[data-discussion-id="${id}"]`;
-
- eventHub.$once('scrollToDiscussion', () => {
- scrollTo(selector);
- // Wait for the discussion scroll before updating to the more specific ID
- setTimeout(() => updateUrlWithNoteId(firstNoteId), 0);
- });
- expandDiscussion({ discussionId: id });
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- * @returns {boolean}
- */
-function discussionJump({ expandDiscussion }, id) {
- const selector = `div.discussion[data-discussion-id="${id}"]`;
- expandDiscussion({ discussionId: id });
- return scrollTo(selector, {
- withoutContext: true,
- offset: window.gon?.features?.movedMrSidebar ? -28 : 0,
- });
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- */
-function switchToDiscussionsTabAndJumpTo(self, id) {
- window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
- setTimeout(() => discussionJump(self, id), 0);
- });
-
- window.mrTabs.tabShown('show');
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {object} discussion Discussion we are jumping to
- */
-function jumpToDiscussion(self, discussion) {
- const { id, diff_discussion: isDiffDiscussion, notes } = discussion;
- const firstNoteId = notes?.[0]?.id;
- if (id) {
- const activeTab = window.mrTabs.currentAction;
-
- if (activeTab === 'diffs' && isDiffDiscussion) {
- diffsJump(self, id, firstNoteId);
- } else {
- switchToDiscussionsTabAndJumpTo(self, id);
- }
- }
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {function} fn Which function used to get the target discussion's id
- */
-function handleDiscussionJump(self, fn) {
- const isDiffView = window.mrTabs.currentAction === 'diffs';
- const targetId = fn(self.currentDiscussionId, isDiffView);
- const discussion = self.getDiscussion(targetId);
- const discussionFilePath = discussion?.diff_file?.file_path;
-
- window.location.hash = '';
-
- if (discussionFilePath) {
- self.scrollToFile({
- path: discussionFilePath,
- });
- }
-
- self.$nextTick(() => {
- jumpToDiscussion(self, discussion);
- self.setCurrentDiscussionId(targetId);
- });
-}
+import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
function getAllDiscussionElements() {
+ const containerEl = window.mrTabs?.currentAction === 'diffs' ? '.diffs' : '.notes';
return Array.from(
- document.querySelectorAll('[data-discussion-id]:not([data-discussion-resolved])'),
+ document.querySelectorAll(
+ `${containerEl} div[data-discussion-id]:not([data-discussion-resolved])`,
+ ),
);
}
@@ -182,14 +58,10 @@ function getPreviousDiscussion() {
}
function handleJumpForBothPages(getDiscussion, ctx, fn, scrollOptions) {
- if (window.mrTabs.currentAction !== 'show') {
- handleDiscussionJump(ctx, fn);
- } else {
- const discussion = getDiscussion();
- const id = discussion.dataset.discussionId;
- ctx.expandDiscussion({ discussionId: id });
- scrollToElement(discussion, scrollOptions);
- }
+ const discussion = getDiscussion();
+ const id = discussion.dataset.discussionId;
+ ctx.expandDiscussion({ discussionId: id });
+ scrollToElement(discussion, scrollOptions);
}
export default {
@@ -205,9 +77,11 @@ export default {
},
methods: {
...mapActions(['expandDiscussion', 'setCurrentDiscussionId']),
- ...mapActions('diffs', ['scrollToFile']),
+ ...mapActions('diffs', ['scrollToFile', 'disableVirtualScroller']),
+
+ async jumpToNextDiscussion(scrollOptions) {
+ await this.disableVirtualScroller();
- jumpToNextDiscussion(scrollOptions) {
handleJumpForBothPages(
getNextDiscussion,
this,
@@ -216,7 +90,9 @@ export default {
);
},
- jumpToPreviousDiscussion(scrollOptions) {
+ async jumpToPreviousDiscussion(scrollOptions) {
+ await this.disableVirtualScroller();
+
handleJumpForBothPages(
getPreviousDiscussion,
this,
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 9783def1b46..44751020173 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
export default {
@@ -46,7 +46,7 @@ export default {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
diff --git a/app/assets/javascripts/notes/timeline.js b/app/assets/javascripts/notes/timeline.js
deleted file mode 100644
index df6d1b21400..00000000000
--- a/app/assets/javascripts/notes/timeline.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import TimelineToggle from './components/timeline_toggle.vue';
-
-export default function initTimelineToggle(store) {
- const el = document.getElementById('js-incidents-timeline-toggle');
-
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- render(createElement) {
- return createElement(TimelineToggle);
- },
- });
-}
diff --git a/app/assets/javascripts/notes/utils/get_notes_filter_data.js b/app/assets/javascripts/notes/utils/get_notes_filter_data.js
new file mode 100644
index 00000000000..6d62ab5e91b
--- /dev/null
+++ b/app/assets/javascripts/notes/utils/get_notes_filter_data.js
@@ -0,0 +1,21 @@
+/**
+ * Returns parsed notes filter data from a given element's dataset
+ *
+ * @param {Element} el containing info in the dataset
+ */
+export const getNotesFilterData = (el) => {
+ const { notesFilterValue: valueData, notesFilters: filtersData } = el.dataset;
+
+ const filtersParsed = filtersData ? JSON.parse(filtersData) : {};
+ const filters = Object.keys(filtersParsed).map((key) => ({
+ title: key,
+ value: filtersParsed[key],
+ }));
+
+ const value = valueData ? Number(valueData) : undefined;
+
+ return {
+ notesFilters: filters,
+ notesFilterValue: value,
+ };
+};