diff options
Diffstat (limited to 'app/assets/javascripts/notes')
33 files changed, 402 insertions, 311 deletions
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue index aaf64702ffd..47d14783d5d 100644 --- a/app/assets/javascripts/notes/components/comment_field_layout.vue +++ b/app/assets/javascripts/notes/components/comment_field_layout.vue @@ -1,6 +1,6 @@ <script> -import EmailParticipantsWarning from './email_participants_warning.vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; +import EmailParticipantsWarning from './email_participants_warning.vue'; const DEFAULT_NOTEABLE_TYPE = 'Issue'; diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 111af977ec5..50db3b86025 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -1,29 +1,27 @@ <script> +import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui'; +import Autosize from 'autosize'; import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; -import { isEmpty } from 'lodash'; -import Autosize from 'autosize'; -import { GlButton, GlIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -import { deprecatedCreateFlash as Flash } from '~/flash'; import Autosave from '~/autosave'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import { deprecatedCreateFlash as Flash } from '~/flash'; import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase, slugifyWithUnderscore, } from '~/lib/utils/text_utility'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; -import * as constants from '../constants'; -import eventHub from '../event_hub'; +import { __, sprintf } from '~/locale'; import markdownField from '~/vue_shared/components/markdown/field.vue'; -import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import noteSignedOutWidget from './note_signed_out_widget.vue'; -import discussionLockedWidget from './discussion_locked_widget.vue'; +import * as constants from '../constants'; +import eventHub from '../event_hub'; import issuableStateMixin from '../mixins/issuable_state'; import CommentFieldLayout from './comment_field_layout.vue'; +import discussionLockedWidget from './discussion_locked_widget.vue'; +import noteSignedOutWidget from './note_signed_out_widget.vue'; export default { name: 'CommentForm', @@ -31,11 +29,14 @@ export default { noteSignedOutWidget, discussionLockedWidget, markdownField, - userAvatarLink, GlButton, TimelineEntryItem, GlIcon, CommentFieldLayout, + GlFormCheckbox, + }, + directives: { + GlTooltip: GlTooltipDirective, }, mixins: [glFeatureFlagsMixin(), issuableStateMixin], props: { @@ -48,8 +49,8 @@ export default { return { note: '', noteType: constants.COMMENT, + noteIsConfidential: false, isSubmitting: false, - isSubmitButtonDisabled: true, }; }, computed: { @@ -82,6 +83,9 @@ export default { canCreateNote() { return this.getNoteableData.current_user.can_create_note; }, + canSetConfidential() { + return this.getNoteableData.current_user.can_update; + }, issueActionButtonTitle() { const openOrClose = this.isOpen ? 'close' : 'reopen'; @@ -145,13 +149,14 @@ export default { trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); }, - }, - watch: { - note(newNote) { - this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); + hasCloseAndCommentButton() { + return !this.glFeatures.removeCommentCloseReopen; }, - isSubmitting(newValue) { - this.setIsSubmitButtonDisabled(this.note, newValue); + confidentialNotesEnabled() { + return Boolean(this.glFeatures.confidentialNotes); + }, + disableSubmitButton() { + return this.note.length === 0 || this.isSubmitting; }, }, mounted() { @@ -172,13 +177,6 @@ export default { 'reopenIssuable', 'toggleIssueLocalState', ]), - setIsSubmitButtonDisabled(note, isSubmitting) { - if (!isEmpty(note) && !isSubmitting) { - this.isSubmitButtonDisabled = false; - } else { - this.isSubmitButtonDisabled = true; - } - }, handleSave(withIssueAction) { if (this.note.length) { const noteData = { @@ -188,6 +186,7 @@ export default { note: { noteable_type: this.noteableType, noteable_id: this.getNoteableData.id, + confidential: this.noteIsConfidential, note: this.note, }, merge_request_diff_head_sha: this.getNoteableData.diff_head_sha, @@ -251,6 +250,7 @@ export default { if (shouldClear) { this.note = ''; + this.noteIsConfidential = false; this.resizeTextarea(); this.$refs.markdownField.previewMarkdown = false; } @@ -301,15 +301,6 @@ export default { <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-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"> <comment-field-layout @@ -348,11 +339,26 @@ export default { </markdown-field> </comment-field-layout> <div class="note-form-actions"> + <gl-form-checkbox + v-if="confidentialNotesEnabled && canSetConfidential" + v-model="noteIsConfidential" + class="gl-mb-6" + data-testid="confidential-note-checkbox" + > + {{ s__('Notes|Make this comment confidential') }} + <gl-icon + v-gl-tooltip:tooltipcontainer.bottom + name="question" + :size="16" + :title="s__('Notes|Confidential comments are only visible to project members')" + class="gl-text-gray-500" + /> + </gl-form-checkbox> <div class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <gl-button - :disabled="isSubmitButtonDisabled" + :disabled="disableSubmitButton" class="js-comment-button js-comment-submit-button" data-qa-selector="comment_button" data-testid="comment-button" @@ -365,7 +371,7 @@ export default { >{{ commentButtonTitle }}</gl-button > <gl-button - :disabled="isSubmitButtonDisabled" + :disabled="disableSubmitButton" name="button" category="primary" variant="success" @@ -384,7 +390,7 @@ export default { class="btn btn-transparent" @click.prevent="setNoteType('comment')" > - <gl-icon name="check" class="icon" /> + <gl-icon name="check" class="icon gl-flex-shrink-0" /> <div class="description"> <strong>{{ __('Comment') }}</strong> <p> @@ -400,10 +406,12 @@ export default { <li class="divider droplab-item-ignore"></li> <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> <button + type="button" + class="btn btn-transparent" data-qa-selector="discussion_menu_item" @click.prevent="setNoteType('discussion')" > - <gl-icon name="check" class="icon" /> + <gl-icon name="check" class="icon gl-flex-shrink-0" /> <div class="description"> <strong>{{ __('Start thread') }}</strong> <p>{{ startDiscussionDescription }}</p> @@ -414,7 +422,7 @@ export default { </div> <gl-button - v-if="canToggleIssueState" + v-if="hasCloseAndCommentButton && canToggleIssueState" :loading="isToggleStateButtonLoading" category="secondary" :variant="buttonVariant" diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue index ee39a529345..0ce1eb8191a 100644 --- a/app/assets/javascripts/notes/components/diff_discussion_header.vue +++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue @@ -1,10 +1,10 @@ <script> -import { mapActions } from 'vuex'; -import { escape } from 'lodash'; import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { escape } from 'lodash'; +import { mapActions } from 'vuex'; -import { s__, __, sprintf } from '~/locale'; import { truncateSha } from '~/lib/utils/text_utility'; +import { s__, __, sprintf } from '~/locale'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteEditedText from './note_edited_text.vue'; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index b7355d4d927..e96e1204f76 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,12 +1,12 @@ <script> /* eslint-disable vue/no-v-html */ -import { mapState, mapActions } from 'vuex'; import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +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 { getDiffMode } from '~/diffs/store/utils'; import { diffViewerModes } from '~/ide/constants'; +import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { isCollapsed } from '../../diffs/utils/diff_file'; const FIRST_CHAR_REGEX = /^(\+|-| )/; diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index da4134ab2c4..27408bc3354 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -1,8 +1,8 @@ <script> +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ReplyPlaceholder from './discussion_reply_placeholder.vue'; import ResolveDiscussionButton from './discussion_resolve_button.vue'; import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'DiscussionActions', diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 0a72627834d..55cf75132a9 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,6 +1,6 @@ <script> -import { mapGetters, mapActions } from 'vuex'; import { GlTooltipDirective, GlIcon, GlButton, GlButtonGroup } from '@gitlab/ui'; +import { mapGetters, mapActions } from 'vuex'; import { __ } from '~/locale'; import discussionNavigation from '../mixins/discussion_navigation'; diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index aa61aa9b3cb..88f053aed67 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,6 +1,6 @@ <script> -import { mapGetters, mapActions } from 'vuex'; import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { mapGetters, mapActions } from 'vuex'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import { DISCUSSION_FILTERS_DEFAULT_VALUE, diff --git a/app/assets/javascripts/notes/components/discussion_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue index facc53e27a6..fa3c900c337 100644 --- a/app/assets/javascripts/notes/components/discussion_navigator.vue +++ b/app/assets/javascripts/notes/components/discussion_navigator.vue @@ -1,8 +1,8 @@ <script> /* global Mousetrap */ import 'mousetrap'; -import discussionNavigation from '~/notes/mixins/discussion_navigation'; import eventHub from '~/notes/event_hub'; +import discussionNavigation from '~/notes/mixins/discussion_navigation'; export default { mixins: [discussionNavigation], diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 8ac915c3c03..0f74d78c8e0 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -1,15 +1,14 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import { SYSTEM_NOTE } from '../constants'; import { __ } from '~/locale'; import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; import SystemNote from '~/vue_shared/components/notes/system_note.vue'; +import { SYSTEM_NOTE } from '../constants'; +import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue'; +import NoteEditedText from './note_edited_text.vue'; import NoteableNote from './noteable_note.vue'; import ToggleRepliesWidget from './toggle_replies_widget.vue'; -import NoteEditedText from './note_edited_text.vue'; -import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'DiscussionNotes', @@ -18,7 +17,6 @@ export default { NoteEditedText, DiscussionNotesRepliesWrapper, }, - mixins: [glFeatureFlagsMixin()], props: { discussion: { type: Object, @@ -96,14 +94,14 @@ export default { return note.isPlaceholderNote ? note.notes[0] : note; }, handleMouseEnter(discussion) { - if (this.glFeatures.multilineComments && discussion.position) { + if (discussion.position) { this.setSelectedCommentPositionHover(discussion.position.line_range); } }, handleMouseLeave(discussion) { - // Even though position isn't used here we still don't want to unecessarily call a mutation + // Even though position isn't used here we still don't want to unnecessarily call a mutation // The lack of position tells us that highlighting is irrelevant in this context - if (this.glFeatures.multilineComments && discussion.position) { + if (discussion.position) { this.setSelectedCommentPositionHover(); } }, diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue index 2ddca56ddd5..dfeda4aae7c 100644 --- a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue +++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue @@ -16,9 +16,14 @@ export default { }, render(h, { props, children }) { if (props.isDiffDiscussion) { - return h('li', { class: 'discussion-collapsible bordered-box clearfix' }, [ - h('ul', { class: 'notes' }, children), - ]); + return h( + 'li', + { + class: + 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base gl-overflow-hidden clearfix', + }, + [h('ul', { class: 'notes' }, children)], + ); } return children; diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue index bb1ff58120a..ecf42fce1d2 100644 --- a/app/assets/javascripts/notes/components/email_participants_warning.vue +++ b/app/assets/javascripts/notes/components/email_participants_warning.vue @@ -1,7 +1,7 @@ <script> import { GlSprintf } from '@gitlab/ui'; -import { s__, sprintf } from '~/locale'; import { toNounSeriesText } from '~/lib/utils/grammar'; +import { s__, sprintf } from '~/locale'; export default { components: { diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue index 9fbf2c9265c..6ad565567be 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 { mapActions, mapState } from 'vuex'; import { GlFormSelect, GlSprintf } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; import { getSymbol, getLineClasses } from './multiline_comment_utils'; export default { diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index b85cfa83e09..907a4316a93 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,13 +1,14 @@ <script> -import { mapGetters } from 'vuex'; import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; -import ReplyButton from './note_actions/reply_button.vue'; -import eventHub from '~/sidebar/event_hub'; +import { mapGetters } from 'vuex'; import Api from '~/api'; +import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; import { deprecatedCreateFlash as flash } from '~/flash'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; +import { __, sprintf } from '~/locale'; +import eventHub from '~/sidebar/event_hub'; import { splitCamelCase } from '../../lib/utils/text_utility'; +import ReplyButton from './note_actions/reply_button.vue'; export default { name: 'NoteActions', @@ -193,7 +194,7 @@ export default { }, closeTooltip() { this.$nextTick(() => { - this.$root.$emit('bv::hide::tooltip'); + this.$root.$emit(BV_HIDE_TOOLTIP); }); }, handleAssigneeUpdate(assignees) { @@ -243,66 +244,62 @@ export default { :title="displayContributorBadgeText" >{{ __('Contributor') }}</span > - <div v-if="canResolve" class="gl-ml-2"> - <gl-button - ref="resolveButton" - v-gl-tooltip - size="small" - category="tertiary" - :variant="resolveVariant" - :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" - :title="resolveButtonTitle" - :aria-label="resolveButtonTitle" - :icon="resolveIcon" - :loading="isResolving" - class="line-resolve-btn note-action-button" - @click="onResolve" - /> - </div> - <div v-if="canAwardEmoji" class="gl-ml-3 gl-mr-2"> - <a - v-gl-tooltip - :class="{ 'js-user-authored': isAuthoredByCurrentUser }" - class="note-action-button note-emoji-button js-add-award js-note-emoji" - href="#" - title="Add reaction" - data-position="right" - > - <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" /> - <gl-icon class="link-highlight award-control-icon-positive" name="smiley" /> - <gl-icon class="link-highlight award-control-icon-super-positive" name="smile" /> - </a> - </div> + <gl-button + v-if="canResolve" + ref="resolveButton" + v-gl-tooltip + size="small" + category="tertiary" + :variant="resolveVariant" + :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" + :title="resolveButtonTitle" + :aria-label="resolveButtonTitle" + :icon="resolveIcon" + :loading="isResolving" + class="line-resolve-btn note-action-button" + @click="onResolve" + /> + <a + v-if="canAwardEmoji" + v-gl-tooltip + :class="{ 'js-user-authored': isAuthoredByCurrentUser }" + class="note-action-button note-emoji-button js-add-award js-note-emoji gl-text-gray-600 gl-m-2" + href="#" + title="Add reaction" + data-position="right" + > + <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" /> + <gl-icon class="link-highlight award-control-icon-positive" name="smiley" /> + <gl-icon class="link-highlight award-control-icon-super-positive" name="smile" /> + </a> <reply-button v-if="showReply" ref="replyButton" class="js-reply-button" @startReplying="$emit('startReplying')" /> - <div v-if="canEdit" class="gl-ml-2"> - <gl-button - v-gl-tooltip - title="Edit comment" - icon="pencil" - size="small" - category="tertiary" - class="note-action-button js-note-edit btn btn-transparent" - data-qa-selector="note_edit_button" - @click="onEdit" - /> - </div> - <div v-if="showDeleteAction" class="gl-ml-2"> - <gl-button - v-gl-tooltip - title="Delete comment" - size="small" - icon="remove" - category="tertiary" - class="note-action-button js-note-delete btn btn-transparent" - @click="onDelete" - /> - </div> - <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions gl-ml-2"> + <gl-button + v-if="canEdit" + v-gl-tooltip + title="Edit comment" + icon="pencil" + size="small" + category="tertiary" + class="note-action-button js-note-edit btn btn-transparent" + data-qa-selector="note_edit_button" + @click="onEdit" + /> + <gl-button + v-if="showDeleteAction" + v-gl-tooltip + title="Delete comment" + size="small" + icon="remove" + category="tertiary" + class="note-action-button js-note-delete btn btn-transparent" + @click="onDelete" + /> + <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions"> <gl-button v-gl-tooltip title="More actions" diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index acbbee13a6d..5ce03091504 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -1,7 +1,11 @@ <script> import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; export default { + i18n: { + buttonText: __('Reply to comment'), + }, name: 'ReplyButton', components: { GlButton, @@ -13,18 +17,15 @@ export default { </script> <template> - <div class="gl-ml-2"> - <gl-button - ref="button" - v-gl-tooltip - data-track-event="click_button" - data-track-label="reply_comment_button" - category="tertiary" - size="small" - icon="comment" - :title="__('Reply to comment')" - :aria-label="__('Reply to comment')" - @click="$emit('startReplying')" - /> - </div> + <gl-button + v-gl-tooltip + data-track-event="click_button" + data-track-label="reply_comment_button" + category="tertiary" + size="small" + icon="comment" + :title="$options.i18n.buttonText" + :aria-label="$options.i18n.buttonText" + @click="$emit('startReplying')" + /> </template> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index cf3e991986c..9eb7b928ea4 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -1,8 +1,8 @@ <script> import { mapActions, mapGetters } from 'vuex'; +import { __ } from '~/locale'; import AwardsList from '~/vue_shared/components/awards_list.vue'; import { deprecatedCreateFlash as Flash } from '../../flash'; -import { __ } from '~/locale'; export default { components: { diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 8855ceac3d5..8c5d81c0cc9 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,14 +1,16 @@ <script> /* eslint-disable vue/no-v-html */ -import { mapActions, mapGetters, mapState } from 'vuex'; import $ from 'jquery'; +import { escape } from 'lodash'; +import { mapActions, mapGetters, mapState } from 'vuex'; + import '~/behaviors/markdown/render_gfm'; -import noteEditedText from './note_edited_text.vue'; -import noteAwardsList from './note_awards_list.vue'; +import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; +import autosave from '../mixins/autosave'; import noteAttachment from './note_attachment.vue'; +import noteAwardsList from './note_awards_list.vue'; +import noteEditedText from './note_edited_text.vue'; import noteForm from './note_form.vue'; -import autosave from '../mixins/autosave'; -import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; export default { components: { @@ -29,6 +31,11 @@ export default { required: false, default: null, }, + file: { + type: Object, + required: false, + default: null, + }, canEdit: { type: Boolean, required: true, @@ -46,6 +53,7 @@ export default { }, computed: { ...mapGetters(['getDiscussion', 'suggestionsCount']), + ...mapGetters('diffs', ['suggestionCommitMessage']), discussion() { if (!this.note.isDraft) return {}; @@ -54,7 +62,6 @@ export default { ...mapState({ batchSuggestionsInfo: (state) => state.notes.batchSuggestionsInfo, }), - ...mapState('diffs', ['defaultSuggestionCommitMessage']), noteBody() { return this.note.note; }, @@ -64,6 +71,21 @@ export default { lineType() { return this.line ? this.line.type : null; }, + commitMessage() { + // Please see this issue comment for why these + // are hard-coded to 1: + // https://gitlab.com/gitlab-org/gitlab/-/issues/291027#note_468308022 + const suggestionsCount = 1; + const filesCount = 1; + const filePaths = this.file ? [this.file.file_path] : []; + const suggestion = this.suggestionCommitMessage({ + file_paths: filePaths.join(', '), + suggestions_count: suggestionsCount, + files_count: filesCount, + }); + + return escape(suggestion); + }, }, mounted() { this.renderGFM(); @@ -135,7 +157,7 @@ export default { :note-html="note.note_html" :line-type="lineType" :help-page-path="helpPagePath" - :default-commit-message="defaultSuggestionCommitMessage" + :default-commit-message="commitMessage" @apply="applySuggestion" @applyBatch="applySuggestionBatch" @addToBatch="addSuggestionToBatch" @@ -156,6 +178,7 @@ export default { @handleFormUpdate="handleFormUpdate" @cancelForm="formCancelHandler" /> + <!-- eslint-disable vue/no-mutating-props --> <textarea v-if="canEdit" v-model="note.note" @@ -163,6 +186,7 @@ export default { class="hidden js-task-list-field" dir="auto" ></textarea> + <!-- eslint-enable vue/no-mutating-props --> <note-edited-text v-if="note.last_edited_at" :edited-at="note.last_edited_at" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 9acb837c27f..653bc450d0b 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,14 +1,15 @@ <script> /* eslint-disable vue/no-v-html */ +import { GlButton } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; +import { getDraft, updateDraft } from '~/lib/utils/autosave'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import eventHub from '../event_hub'; +import { __, sprintf } 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'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; -import { __, sprintf } from '~/locale'; -import { getDraft, updateDraft } from '~/lib/utils/autosave'; import CommentFieldLayout from './comment_field_layout.vue'; export default { @@ -16,6 +17,7 @@ export default { components: { markdownField, CommentFieldLayout, + GlButton, }, mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable], props: { @@ -378,61 +380,70 @@ export default { </template> </label> </p> - <div> - <button + <div class="gl-display-sm-flex gl-flex-wrap"> + <gl-button :disabled="isDisabled" - type="button" - class="btn btn-success" + category="primary" + variant="success" + class="gl-mr-3" data-qa-selector="start_review_button" @click="handleAddToReview" > <template v-if="hasDrafts">{{ __('Add to review') }}</template> <template v-else>{{ __('Start a review') }}</template> - </button> - <button + </gl-button> + <gl-button :disabled="isDisabled" - type="button" - class="btn js-comment-button" + category="secondary" + variant="default" data-qa-selector="comment_now_button" + class="gl-mr-3 js-comment-button" @click="handleUpdate()" > {{ __('Add comment now') }} - </button> - <button - class="btn note-edit-cancel js-close-discussion-note-form" - type="button" + </gl-button> + <gl-button + class="note-edit-cancel js-close-discussion-note-form" + category="secondary" + variant="default" data-testid="cancelBatchCommentsEnabled" @click="cancelHandler(true)" > {{ __('Cancel') }} - </button> + </gl-button> </div> </template> <template v-else> - <button - :disabled="isDisabled" - type="button" - class="js-vue-issue-save btn btn-success js-comment-button" - data-qa-selector="reply_comment_button" - @click="handleUpdate()" - > - {{ saveButtonTitle }} - </button> - <button - v-if="discussion.resolvable" - class="btn btn-default gl-mr-3 js-comment-resolve-button" - @click.prevent="handleUpdate(true)" - > - {{ resolveButtonTitle }} - </button> - <button - class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" - type="button" - data-testid="cancel" - @click="cancelHandler(true)" - > - {{ __('Cancel') }} - </button> + <div class="gl-display-sm-flex gl-flex-wrap"> + <gl-button + :disabled="isDisabled" + category="primary" + variant="success" + data-qa-selector="reply_comment_button" + class="gl-mr-3 js-vue-issue-save js-comment-button" + @click="handleUpdate()" + > + {{ saveButtonTitle }} + </gl-button> + <gl-button + v-if="discussion.resolvable" + category="secondary" + variant="default" + class="gl-mr-3 js-comment-resolve-button" + @click.prevent="handleUpdate(true)" + > + {{ resolveButtonTitle }} + </gl-button> + <gl-button + class="note-edit-cancel js-close-discussion-note-form" + category="secondary" + variant="default" + data-testid="cancel" + @click="cancelHandler(true)" + > + {{ __('Cancel') }} + </gl-button> + </div> </template> </div> </form> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 17a995018d3..6932af61c69 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable vue/no-v-html */ +import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import { mapActions } from 'vuex'; -import { GlIcon, GlLoadingIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui'; -import { isUserBusy } from '~/set_status_modal/utils'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import UserNameWithStatus from '../../sidebar/components/assignees/user_name_with_status.vue'; export default { components: { @@ -12,7 +12,7 @@ export default { import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'), GlIcon, GlLoadingIcon, - GlSprintf, + UserNameWithStatus, }, directives: { GlTooltip: GlTooltipDirective, @@ -90,10 +90,6 @@ export default { } return false; }, - authorIsBusy() { - const { status } = this.author; - return status?.availability && isUserBusy(status.availability); - }, emojiElement() { return this.$refs?.authorStatus?.querySelector('gl-emoji'); }, @@ -133,6 +129,9 @@ export default { this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave')); this.isUsernameLinkHovered = false; }, + userAvailability(selectedAuthor) { + return selectedAuthor?.availability || ''; + }, }, }; </script> @@ -158,12 +157,11 @@ export default { :data-username="author.username" > <slot name="note-header-info"></slot> - <span class="note-header-author-name gl-font-weight-bold"> - <gl-sprintf v-if="authorIsBusy" :message="s__('UserAvailability|%{author} (Busy)')"> - <template #author>{{ authorName }}</template> - </gl-sprintf> - <template v-else>{{ authorName }}</template> - </span> + <user-name-with-status + :name="authorName" + :availability="userAvailability(author)" + container-classes="note-header-author-name gl-font-weight-bold" + /> </a> <span v-if="authorStatus" @@ -210,9 +208,9 @@ export default { v-gl-tooltip:tooltipcontainer.bottom data-testid="confidentialIndicator" name="eye-slash" - :size="14" - :title="s__('Notes|Private comments are accessible by internal staff only')" - class="gl-ml-1 gl-text-gray-700 align-middle" + :size="16" + :title="s__('Notes|This comment is confidential and only visible to project members')" + class="gl-ml-1 gl-text-orange-700 align-middle" /> <slot name="extra-controls"></slot> <gl-loading-icon diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 0a9a3da6069..34dd21dcbac 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,22 +1,22 @@ <script> -import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; -import { s__, __ } from '~/locale'; +import { mapActions, mapGetters } from 'vuex'; +import DraftNote from '~/batch_comments/components/draft_note.vue'; import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; +import { s__, __ } from '~/locale'; +import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -import DraftNote from '~/batch_comments/components/draft_note.vue'; import { deprecatedCreateFlash as Flash } from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import diffDiscussionHeader from './diff_discussion_header.vue'; -import noteSignedOutWidget from './note_signed_out_widget.vue'; -import noteForm from './note_form.vue'; -import diffWithNote from './diff_with_note.vue'; +import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; -import eventHub from '../event_hub'; -import DiscussionNotes from './discussion_notes.vue'; +import diffDiscussionHeader from './diff_discussion_header.vue'; +import diffWithNote from './diff_with_note.vue'; import DiscussionActions from './discussion_actions.vue'; +import DiscussionNotes from './discussion_notes.vue'; +import noteForm from './note_form.vue'; +import noteSignedOutWidget from './note_signed_out_widget.vue'; export default { name: 'NoteableDiscussion', @@ -265,16 +265,8 @@ export default { <div v-else-if="showReplies" :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder clearfix" + class="discussion-reply-holder gl-border-t-0! clearfix" > - <user-avatar-link - v-if="!isReplying && userCanReply" - :link-href="currentUser.path" - :img-src="currentUser.avatar_url" - :img-alt="currentUser.name" - :img-size="40" - class="d-none d-sm-block" - /> <discussion-actions v-if="!isReplying && userCanReply" :discussion="discussion" @@ -285,27 +277,18 @@ export default { @showReplyForm="showReplyForm" @resolve="resolveHandler" /> - <div v-if="isReplying" class="avatar-note-form-holder"> - <user-avatar-link - v-if="currentUser" - :link-href="currentUser.path" - :img-src="currentUser.avatar_url" - :img-alt="currentUser.name" - :img-size="40" - class="d-none d-sm-block" - /> - <note-form - ref="noteForm" - :discussion="discussion" - :is-editing="false" - :line="diffLine" - save-button-title="Comment" - :autosave-key="autosaveKey" - @handleFormUpdateAddToReview="addReplyToReview" - @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" - /> - </div> + <note-form + v-if="isReplying" + ref="noteForm" + :discussion="discussion" + :is-editing="false" + :line="diffLine" + save-button-title="Comment" + :autosave-key="autosaveKey" + @handleFormUpdateAddToReview="addReplyToReview" + @handleFormUpdate="saveReply" + @cancelForm="cancelReplyForm" + /> <note-signed-out-widget v-if="!isLoggedIn" /> </div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index eaa64cf7c01..4343fac3cfa 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -1,21 +1,18 @@ <script> +import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; -import { mapGetters, mapActions } from 'vuex'; import { escape } from 'lodash'; -import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { mapGetters, mapActions } from 'vuex'; +import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants'; +import httpStatusCodes from '~/lib/utils/http_status'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -import { __, s__, sprintf } from '../../locale'; import { deprecatedCreateFlash as Flash } from '../../flash'; +import { __, s__, sprintf } from '../../locale'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import noteHeader from './note_header.vue'; -import noteActions from './note_actions.vue'; -import NoteBody from './note_body.vue'; import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; -import httpStatusCodes from '~/lib/utils/http_status'; import { getStartLineNumber, getEndLineNumber, @@ -23,7 +20,9 @@ import { commentLineOptions, formatLineRange, } from './multiline_comment_utils'; -import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants'; +import noteActions from './note_actions.vue'; +import NoteBody from './note_body.vue'; +import noteHeader from './note_header.vue'; export default { name: 'NoteableNote', @@ -38,7 +37,7 @@ export default { directives: { SafeHtml, }, - mixins: [noteable, resolvable, glFeatureFlagsMixin()], + mixins: [noteable, resolvable], props: { note: { type: Object, @@ -160,7 +159,6 @@ export default { }, showMultiLineComment() { if ( - !this.glFeatures.multilineComments || !this.discussionRoot || this.startLineNumber.length === 0 || this.endLineNumber.length === 0 @@ -289,6 +287,7 @@ export default { }; this.isRequesting = true; this.oldContent = this.note.note_html; + // eslint-disable-next-line vue/no-mutating-props this.note.note_html = escape(noteText); this.updateNote(data) @@ -321,6 +320,7 @@ export default { } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { + // eslint-disable-next-line vue/no-mutating-props this.note.note_html = this.oldContent; this.oldContent = null; } @@ -330,6 +330,7 @@ export default { recoverNoteContent(noteText) { // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content + // eslint-disable-next-line vue/no-mutating-props this.note.note = noteText; const { noteBody } = this.$refs; if (noteBody) { @@ -428,6 +429,7 @@ export default { ref="noteBody" :note="note" :line="line" + :file="diffFile" :can-edit="note.current_user.can_edit" :is-editing="isEditing" :help-page-path="helpPagePath" diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index e9e687a8743..2d66e0d24e3 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,21 +1,22 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; +import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; +import { __ } from '~/locale'; +import initUserPopovers from '~/user_popovers'; +import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { deprecatedCreateFlash as Flash } from '../../flash'; -import * as constants from '../constants'; -import eventHub from '../event_hub'; -import noteableNote from './noteable_note.vue'; -import noteableDiscussion from './noteable_discussion.vue'; -import discussionFilterNote from './discussion_filter_note.vue'; -import systemNote from '../../vue_shared/components/notes/system_note.vue'; -import commentForm from './comment_form.vue'; +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'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; -import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; -import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; -import { __ } from '~/locale'; -import initUserPopovers from '~/user_popovers'; +import systemNote from '../../vue_shared/components/notes/system_note.vue'; +import * as constants from '../constants'; +import eventHub from '../event_hub'; +import commentForm from './comment_form.vue'; +import discussionFilterNote from './discussion_filter_note.vue'; +import noteableDiscussion from './noteable_discussion.vue'; +import noteableNote from './noteable_note.vue'; export default { name: 'NotesApp', @@ -30,6 +31,7 @@ export default { discussionFilterNote, OrderedLayout, }, + mixins: [glFeatureFlagsMixin()], props: { noteableData: { type: Object, @@ -57,7 +59,6 @@ export default { }, data() { return { - isFetching: false, currentFilter: null, }; }, @@ -68,6 +69,7 @@ export default { 'convertedDisscussionIds', 'getNotesDataByProp', 'isLoading', + 'isFetching', 'commentsDisabled', 'getNoteableData', 'userCanReply', @@ -103,6 +105,13 @@ export default { }, }, watch: { + async isFetching() { + if (!this.isFetching) { + await this.$nextTick(); + await this.startTaskList(); + await this.checkLocationHash(); + } + }, shouldShow() { if (!this.isNotesFetched) { this.fetchNotes(); @@ -153,6 +162,7 @@ export default { }, methods: { ...mapActions([ + 'setFetchingState', 'setLoadingState', 'fetchDiscussions', 'poll', @@ -183,7 +193,11 @@ export default { fetchNotes() { if (this.isFetching) return null; - this.isFetching = true; + this.setFetchingState(true); + + if (this.glFeatures.paginatedNotes) { + return this.initPolling(); + } return this.fetchDiscussions(this.getFetchDiscussionsConfig()) .then(this.initPolling) @@ -191,11 +205,8 @@ export default { this.setLoadingState(false); this.setNotesFetchedState(true); eventHub.$emit('fetchedNotesData'); - this.isFetching = false; + this.setFetchingState(false); }) - .then(this.$nextTick) - .then(this.startTaskList) - .then(this.checkLocationHash) .catch(() => { this.setLoadingState(false); this.setNotesFetchedState(true); diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue index c279a7107c7..ed1f456c174 100644 --- a/app/assets/javascripts/notes/components/sort_discussion.vue +++ b/app/assets/javascripts/notes/components/sort_discussion.vue @@ -2,8 +2,8 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { mapActions, mapGetters } from 'vuex'; import { __ } from '~/locale'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import Tracking from '~/tracking'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { ASC, DESC } from '../constants'; const SORT_OPTIONS = [ diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue index 8162878f80d..87d22e5b986 100644 --- a/app/assets/javascripts/notes/components/timeline_toggle.vue +++ b/app/assets/javascripts/notes/components/timeline_toggle.vue @@ -2,9 +2,9 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { mapActions, mapGetters } from 'vuex'; import { s__ } from '~/locale'; +import TrackEventDirective from '~/vue_shared/directives/track_event'; import { COMMENTS_ONLY_FILTER_VALUE, DESC } from '../constants'; import notesEventHub from '../event_hub'; -import TrackEventDirective from '~/vue_shared/directives/track_event'; import { trackToggleTimelineView } from '../utils'; export const timelineEnabledTooltip = s__('Timeline|Turn timeline view off'); diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue index ab7fa793bdc..01e3f84d00e 100644 --- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue +++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue @@ -1,8 +1,8 @@ <script> -import { uniqBy } from 'lodash'; import { GlButton, GlIcon } from '@gitlab/ui'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { uniqBy } from 'lodash'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; export default { components: { @@ -39,13 +39,17 @@ export default { this.$emit('toggle'); }, }, + ICON_CLASS: 'gl-mr-3 gl-cursor-pointer', }; </script> <template> - <li :class="className" class="replies-toggle js-toggle-replies"> + <li + :class="className" + class="replies-toggle js-toggle-replies gl-display-flex! gl-align-items-center gl-flex-wrap" + > <template v-if="collapsed"> - <gl-icon name="chevron-right" @click.native="toggle" /> + <gl-icon :class="$options.ICON_CLASS" name="chevron-right" @click.native="toggle" /> <div> <user-avatar-link v-for="author in uniqueAuthors" @@ -59,7 +63,7 @@ export default { /> </div> <gl-button - class="js-replies-text" + class="js-replies-text gl-mr-2" category="tertiary" variant="link" data-qa-selector="expand_replies_button" @@ -68,18 +72,19 @@ export default { {{ replies.length }} {{ n__('reply', 'replies', replies.length) }} </gl-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 gl-mx-2"> {{ lastReply.author.name }} </a> <time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" /> </template> - <span + <div v-else - class="collapse-replies-btn js-collapse-replies" + class="collapse-replies-btn js-collapse-replies gl-display-flex align-items-center" data-qa-selector="collapse_replies_button" @click="toggle" > - <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }} - </span> + <gl-icon :class="$options.ICON_CLASS" name="chevron-down" /> + <span class="gl-cursor-pointer">{{ s__('Notes|Collapse replies') }}</span> + </div> </li> </template> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 1f0b2afab9e..e4241669fbc 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -2,8 +2,8 @@ import Vue from 'vue'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; import initSortDiscussions from './sort_discussions'; -import initTimelineToggle from './timeline'; import { store } from './stores'; +import initTimelineToggle from './timeline'; const el = document.getElementById('js-vue-notes'); diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index b161773f5f1..d670d0bd4c5 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -1,7 +1,7 @@ import $ from 'jquery'; +import { s__ } from '~/locale'; import Autosave from '../../autosave'; import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; -import { s__ } from '~/locale'; export default { methods: { 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 5ce541781d4..76342e07c04 100644 --- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js +++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js @@ -2,8 +2,8 @@ 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 { deprecatedCreateFlash as createFlash } from '~/flash'; -import { s__ } from '~/locale'; import { clearDraft } from '~/lib/utils/autosave'; +import { s__ } from '~/locale'; import { formatLineRange } from '~/notes/components/multiline_comment_utils'; export default { diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index c6684efed4d..19403c29cda 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,23 +1,24 @@ -import Vue from 'vue'; import $ from 'jquery'; import Visibility from 'visibilityjs'; +import Vue from 'vue'; +import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; -import TaskList from '../../task_list'; -import { deprecatedCreateFlash as Flash } from '../../flash'; -import Poll from '../../lib/utils/poll'; -import * as types from './mutation_types'; -import * as utils from './utils'; -import * as constants from '../constants'; +import { __, sprintf } from '~/locale'; +import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql'; +import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql'; +import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql'; import loadAwardsHandler from '../../awards_handler'; -import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +import { deprecatedCreateFlash as Flash } from '../../flash'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; +import Poll from '../../lib/utils/poll'; import { mergeUrlParams } from '../../lib/utils/url_utility'; +import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +import TaskList from '../../task_list'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; -import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql'; -import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql'; -import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql'; -import { __, sprintf } from '~/locale'; -import Api from '~/api'; +import * as constants from '../constants'; +import eventHub from '../event_hub'; +import * as types from './mutation_types'; +import * as utils from './utils'; let eTagPoll; @@ -420,14 +421,25 @@ export const saveNote = ({ commit, dispatch }, noteData) => { .catch(processErrors); }; -const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { +export const setFetchingState = ({ commit }, fetchingState) => + commit(types.SET_NOTES_FETCHING_STATE, fetchingState); + +const pollSuccessCallBack = async (resp, commit, state, getters, dispatch) => { if (state.isResolvingDiscussion) { return null; } + if (window.gon?.features?.paginatedNotes && !resp.more && state.isFetching) { + eventHub.$emit('fetchedNotesData'); + dispatch('setFetchingState', false); + dispatch('setNotesFetchedState', true); + dispatch('setLoadingState', false); + } + if (resp.notes?.length) { - dispatch('updateOrCreateNotes', resp.notes); + await dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); + dispatch('updateResolvableDiscussionsCounts'); } commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at); @@ -727,9 +739,13 @@ export const updateConfidentialityOnIssuable = ( }) .then(({ data }) => { const { - issueSetConfidential: { issue }, + issueSetConfidential: { issue, errors }, } = data; - setConfidentiality({ commit }, issue.confidential); + if (errors?.length) { + Flash(errors[0], 'alert'); + } else { + setConfidentiality({ commit }, issue.confidential); + } }); }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5891a2e63e3..43d99937b8d 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -48,6 +48,8 @@ export const persistSortOrder = (state) => state.persistSortOrder; export const timelineEnabled = (state) => state.isTimelineEnabled; +export const isFetching = (state) => state.isFetching; + export const isLoading = (state) => state.isLoading; export const getNotesDataByProp = (state) => (prop) => state.notesData[prop]; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 144a3d7ba90..f154edd3434 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -1,7 +1,7 @@ +import { ASC } from '../../constants'; import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; -import { ASC } from '../../constants'; export default () => ({ state: { @@ -47,6 +47,7 @@ export default () => ({ unresolvedDiscussionsCount: 0, descriptionVersions: {}, isTimelineEnabled: false, + isFetching: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 5c4f62f4575..2e8b728e013 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const UPDATE_DISCUSSION_POSITION = 'UPDATE_DISCUSSION_POSITION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; +export const SET_NOTES_FETCHING_STATE = 'SET_NOTES_FETCHING_STATE'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 2c51ce0d970..c5fa34dfedd 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -1,7 +1,8 @@ -import * as utils from './utils'; -import * as types from './mutation_types'; -import * as constants from '../constants'; +import { isEqual } from 'lodash'; import { isInMRPage } from '../../lib/utils/common_utils'; +import * as constants from '../constants'; +import * as types from './mutation_types'; +import * as utils from './utils'; export default { [types.ADD_NEW_NOTE](state, data) { @@ -31,7 +32,22 @@ export default { } } - note.base_discussion = undefined; // No point keeping a reference to this + if (window.gon?.features?.paginatedNotes && note.base_discussion) { + if (discussion.diff_file) { + discussion.file_hash = discussion.diff_file.file_hash; + + discussion.truncated_diff_lines = utils.prepareDiffLines( + discussion.truncated_diff_lines || [], + ); + } + + discussion.resolvable = note.resolvable; + discussion.expanded = note.base_discussion.expanded; + discussion.resolved = note.resolved; + } + + // note.base_discussion = undefined; // No point keeping a reference to this + delete note.base_discussion; discussion.notes = [note]; state.discussions.push(discussion); @@ -220,6 +236,11 @@ export default { [types.UPDATE_NOTE](state, note) { const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); + // Disable eslint here so we can delete the property that we no longer need + // in the note object + // eslint-disable-next-line no-param-reassign + delete note.base_discussion; + if (noteObj.individual_note) { if (note.type === constants.DISCUSSION_NOTE) { noteObj.individual_note = false; @@ -228,7 +249,10 @@ export default { noteObj.notes.splice(0, 1, note); } else { const comment = utils.findNoteObjectById(noteObj.notes, note.id); - noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); + + if (!isEqual(comment, note)) { + noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); + } } }, @@ -313,6 +337,10 @@ export default { state.isLoading = value; }, + [types.SET_NOTES_FETCHING_STATE](state, value) { + state.isFetching = value; + }, + [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 6df926e1249..627e405c75c 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -1,7 +1,7 @@ -import AjaxCache from '~/lib/utils/ajax_cache'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; -import { sprintf, __ } from '~/locale'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import { sprintf, __ } from '~/locale'; // factory function because global flag makes RegExp stateful const createQuickActionsRegex = () => /^\/\w+.*$/gm; |