diff options
Diffstat (limited to 'app/assets/javascripts/notes/components')
6 files changed, 104 insertions, 59 deletions
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 831e6dd8f92..33819c78c0f 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -78,8 +78,8 @@ export default { v-if="resolveAllDiscussionsIssuePath && !allResolved" v-gl-tooltip :href="resolveAllDiscussionsIssuePath" - :title="s__('Create issue to resolve all threads')" - :aria-label="s__('Create issue to resolve all threads')" + :title="__('Create issue to resolve all threads')" + :aria-label="__('Create issue to resolve all threads')" class="new-issue-for-discussion discussion-create-issue-btn" icon="issue-new" /> diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 6fcfa66ea49..d1df4eb848b 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -1,5 +1,6 @@ <script> import { mapGetters, mapActions } from 'vuex'; +import { GlIntersectionObserver } from '@gitlab/ui'; import { __ } from '~/locale'; import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; @@ -16,7 +17,9 @@ export default { ToggleRepliesWidget, NoteEditedText, DiscussionNotesRepliesWrapper, + GlIntersectionObserver, }, + inject: ['discussionObserverHandler'], props: { discussion: { type: Object, @@ -54,7 +57,11 @@ export default { }, }, computed: { - ...mapGetters(['userCanReply']), + ...mapGetters([ + 'userCanReply', + 'previousUnresolvedDiscussionId', + 'firstUnresolvedDiscussionId', + ]), hasReplies() { return Boolean(this.replies.length); }, @@ -77,9 +84,20 @@ export default { url: this.discussion.discussion_path, }; }, + isFirstUnresolved() { + return this.firstUnresolvedDiscussionId === this.discussion.id; + }, + }, + observerOptions: { + threshold: 0, + rootMargin: '0px 0px -50% 0px', }, methods: { - ...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']), + ...mapActions([ + 'toggleDiscussion', + 'setSelectedCommentPositionHover', + 'setCurrentDiscussionId', + ]), componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { @@ -110,6 +128,18 @@ export default { this.setSelectedCommentPositionHover(); } }, + observerTriggered(entry) { + this.discussionObserverHandler({ + entry, + isFirstUnresolved: this.isFirstUnresolved, + currentDiscussion: { ...this.discussion }, + isDiffsPage: !this.isOverviewTab, + functions: { + setCurrentDiscussionId: this.setCurrentDiscussionId, + getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId, + }, + }); + }, }, }; </script> @@ -122,33 +152,35 @@ export default { @mouseleave="handleMouseLeave(discussion)" > <template v-if="shouldGroupReplies"> - <component - :is="componentName(firstNote)" - :note="componentData(firstNote)" - :line="line || diffLine" - :discussion-file="discussion.diff_file" - :commit="commit" - :help-page-path="helpPagePath" - :show-reply-button="userCanReply" - :discussion-root="true" - :discussion-resolve-path="discussion.resolve_path" - :is-overview-tab="isOverviewTab" - @handleDeleteNote="$emit('deleteNote')" - @startReplying="$emit('startReplying')" - > - <template #discussion-resolved-text> - <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 discussion-resolved-text" - /> - </template> - <template #avatar-badge> - <slot name="avatar-badge"></slot> - </template> - </component> + <gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered"> + <component + :is="componentName(firstNote)" + :note="componentData(firstNote)" + :line="line || diffLine" + :discussion-file="discussion.diff_file" + :commit="commit" + :help-page-path="helpPagePath" + :show-reply-button="userCanReply" + :discussion-root="true" + :discussion-resolve-path="discussion.resolve_path" + :is-overview-tab="isOverviewTab" + @handleDeleteNote="$emit('deleteNote')" + @startReplying="$emit('startReplying')" + > + <template #discussion-resolved-text> + <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 discussion-resolved-text" + /> + </template> + <template #avatar-badge> + <slot name="avatar-badge"></slot> + </template> + </component> + </gl-intersection-observer> <discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion"> <toggle-replies-widget v-if="hasReplies" diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue index 6ad565567be..1633b79c3be 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_form.vue +++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue @@ -1,6 +1,6 @@ <script> import { GlFormSelect, GlSprintf } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; +import { mapActions } from 'vuex'; import { getSymbol, getLineClasses } from './multiline_comment_utils'; export default { @@ -27,13 +27,12 @@ export default { }; }, computed: { - ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }), lineNumber() { return this.commentLineOptions[this.commentLineOptions.length - 1].text; }, }, created() { - const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line; + const line = this.lineRange?.start || this.line; this.commentLineStart = { line_code: line.line_code, @@ -42,7 +41,6 @@ export default { new_line: line.new_line, }; - if (this.selectedCommentPosition) return; this.highlightSelection(); }, destroyed() { diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 1ce1696e332..c09582d6287 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { GlSafeHtmlDirective } from '@gitlab/ui'; import { escape } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; @@ -19,6 +20,9 @@ export default { noteForm, Suggestions, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, mixins: [autosave], props: { note: { @@ -144,6 +148,9 @@ export default { this.removeSuggestionInfoFromBatch(suggestionId); }, }, + safeHtmlConfig: { + ADD_TAGS: ['use', 'gl-emoji'], + }, }; </script> @@ -163,11 +170,7 @@ export default { @addToBatch="addSuggestionToBatch" @removeFromBatch="removeSuggestionFromBatch" /> - <div - v-else - class="note-text md" - v-html="note.note_html /* eslint-disable-line vue/no-v-html */" - ></div> + <div v-else v-safe-html:[$options.safeHtmlConfig]="note.note_html" class="note-text md"></div> <note-form v-if="isEditing" ref="noteForm" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index b05643e5e13..d6b65ed0e8b 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,9 +1,9 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlSprintf, GlLink } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; import { getDraft, updateDraft } from '~/lib/utils/autosave'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import { __, sprintf } from '~/locale'; +import { __ } from '~/locale'; import markdownField from '~/vue_shared/components/markdown/field.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../event_hub'; @@ -17,6 +17,8 @@ export default { markdownField, CommentFieldLayout, GlButton, + GlSprintf, + GlLink, }, mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable], props: { @@ -203,16 +205,12 @@ export default { ); }, changedCommentText() { - return sprintf( - __( + return { + text: __( 'This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost.', ), - { - startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`, - endTag: '</a>', - }, - false, - ); + placeholder: { link: ['startTag', 'endTag'] }, + }; }, }, watch: { @@ -318,11 +316,13 @@ export default { <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" - v-html="changedCommentText /* eslint-disable-line vue/no-v-html */" - ></div> + <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> + <gl-sprintf :message="changedCommentText.text" :placeholders="changedCommentText.placeholder"> + <template #link="{ content }"> + <gl-link :href="noteHash" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> <comment-field-layout @@ -334,13 +334,13 @@ export default { :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :line="line" + :lines="lines" :note="discussionNote" :can-suggest="canSuggest" :add-spacing-classes="false" :help-page-path="helpPagePath" :show-suggest-popover="showSuggestPopover" :textarea-value="updatedNoteBody" - :lines="lines" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" > <template #textarea> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 58570e76795..3ab3e7a20d4 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -8,6 +8,7 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import draftNote from '../../batch_comments/components/draft_note.vue'; +import { discussionIntersectionObserverHandlerFactory } from '../../diffs/utils/discussions'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; @@ -38,6 +39,9 @@ export default { TimelineEntryItem, }, mixins: [glFeatureFlagsMixin()], + provide: { + discussionObserverHandler: discussionIntersectionObserverHandlerFactory(), + }, props: { noteableData: { type: Object, @@ -94,15 +98,17 @@ export default { return this.noteableData.noteableType; }, allDiscussions() { + let skeletonNotes = []; + if (this.renderSkeleton || this.isLoading) { const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0; - return new Array(prerenderedNotesCount).fill({ + skeletonNotes = new Array(prerenderedNotesCount).fill({ isSkeletonNote: true, }); } - return this.discussions; + return this.discussions.concat(skeletonNotes); }, canReply() { return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled; @@ -258,7 +264,13 @@ export default { getFetchDiscussionsConfig() { const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') }; - if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) { + const currentFilter = + this.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE; + + if ( + doesHashExistInUrl(constants.NOTE_UNDERSCORE) && + currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE + ) { return { ...defaultConfig, filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE, |