diff options
Diffstat (limited to 'app/assets/javascripts/notes')
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, + }; +}; |