diff options
Diffstat (limited to 'app/assets/javascripts/notes/components')
6 files changed, 173 insertions, 43 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 9a809b71a58..a070cf8866a 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -3,6 +3,7 @@ import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import { isEmpty } from 'lodash'; import Autosize from 'autosize'; +import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; @@ -34,6 +35,10 @@ export default { userAvatarLink, loadingButton, TimelineEntryItem, + GlAlert, + GlIntersperse, + GlLink, + GlSprintf, }, mixins: [issuableStateMixin], props: { @@ -57,8 +62,9 @@ export default { 'getNoteableData', 'getNotesData', 'openState', + 'getBlockedByIssues', ]), - ...mapState(['isToggleStateButtonLoading']), + ...mapState(['isToggleStateButtonLoading', 'isToggleBlockedIssueWarning']), noteableDisplayName() { return splitCamelCase(this.noteableType).toLowerCase(); }, @@ -159,6 +165,7 @@ export default { 'reopenIssue', 'toggleIssueLocalState', 'toggleStateButtonLoading', + 'toggleBlockedIssueWarning', ]), setIsSubmitButtonDisabled(note, isSubmitting) { if (!isEmpty(note) && !isSubmitting) { @@ -220,22 +227,17 @@ export default { this.isSubmitting = false; }, toggleIssueState() { + if ( + this.noteableType.toLowerCase() === constants.ISSUE_NOTEABLE_TYPE && + this.isOpen && + this.getBlockedByIssues && + this.getBlockedByIssues.length > 0 + ) { + this.toggleBlockedIssueWarning(true); + return; + } if (this.isOpen) { - this.closeIssue() - .then(() => { - this.enableButton(); - refreshUserMergeRequestCounts(); - }) - .catch(() => { - this.enableButton(); - this.toggleStateButtonLoading(false); - Flash( - sprintf( - __('Something went wrong while closing the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, - ), - ); - }); + this.forceCloseIssue(); } else { this.reopenIssue() .then(() => { @@ -258,6 +260,23 @@ export default { }); } }, + forceCloseIssue() { + this.closeIssue() + .then(() => { + this.enableButton(); + refreshUserMergeRequestCounts(); + }) + .catch(() => { + this.enableButton(); + this.toggleStateButtonLoading(false); + Flash( + sprintf( + __('Something went wrong while closing the %{issuable}. Please try again later'), + { issuable: this.noteableDisplayName }, + ), + ); + }); + }, discard(shouldClear = true) { // `blur` is needed to clear slash commands autocomplete cache if event fired. // `focus` is needed to remain cursor in the textarea. @@ -361,6 +380,36 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" > </textarea> </markdown-field> + <gl-alert + v-if="isToggleBlockedIssueWarning" + class="prepend-top-16" + :title="__('Are you sure you want to close this blocked issue?')" + :primary-button-text="__('Yes, close issue')" + :secondary-button-text="__('Cancel')" + variant="warning" + :dismissible="false" + @primaryAction="forceCloseIssue" + @secondaryAction="toggleBlockedIssueWarning(false) && enableButton()" + > + <p> + <gl-sprintf + :message=" + __('This issue is currently blocked by the following issues: %{issues}.') + " + > + <template #issues> + <gl-intersperse> + <gl-link + v-for="blockingIssue in getBlockedByIssues" + :key="blockingIssue.web_url" + :href="blockingIssue.web_url" + >#{{ blockingIssue.iid }}</gl-link + > + </gl-intersperse> + </template> + </gl-sprintf> + </p> + </gl-alert> <div class="note-form-actions"> <div class="float-left btn-group @@ -427,7 +476,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" </div> <loading-button - v-if="canToggleIssueState" + v-if="canToggleIssueState && !isToggleBlockedIssueWarning" :loading="isToggleStateButtonLoading" :container-class="[ actionButtonClassNames, diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 07952f9edd9..4a1a1086329 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -29,9 +29,6 @@ export default { resolveAllDiscussionsIssuePath() { return this.getNoteableData.create_issue_to_resolve_discussions_path; }, - resolvedDiscussionsCount() { - return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount; - }, toggeableDiscussions() { return this.discussions.filter(discussion => !discussion.individual_note); }, @@ -60,15 +57,15 @@ export default { <div class="full-width-mobile d-flex d-sm-flex"> <div class="line-resolve-all"> <span - :class="{ 'is-active': allResolved }" - class="line-resolve-btn is-disabled" - type="button" + :class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }" > - <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" /> - </span> - <span class="line-resolve-text"> - {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }} - {{ n__('thread resolved', 'threads resolved', resolvableDiscussionsCount) }} + <template v-if="allResolved"> + <icon name="check-circle-filled" /> + {{ __('All threads resolved') }} + </template> + <template v-else> + {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }} + </template> </span> </div> <div diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index b024884bea0..21d0bffdf1c 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -328,7 +328,8 @@ export default { <button class="btn note-edit-cancel js-close-discussion-note-form" type="button" - @click="cancelHandler()" + data-testid="cancelBatchCommentsEnabled" + @click="cancelHandler(true)" > {{ __('Cancel') }} </button> @@ -353,7 +354,8 @@ export default { <button class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" - @click="cancelHandler()" + data-testid="cancel" + @click="cancelHandler(true)" > {{ __('Cancel') }} </button> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index f82b3554cac..81812ee2279 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -1,12 +1,17 @@ <script> import { mapActions } from 'vuex'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'; export default { components: { timeAgoTooltip, - GitlabTeamMemberBadge, + GitlabTeamMemberBadge: () => + import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'), + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { author: { @@ -44,6 +49,18 @@ export default { required: false, default: true, }, + isConfidential: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isUsernameLinkHovered: false, + emojiTitle: '', + authorStatusHasTooltip: false, + }; }, computed: { toggleChevronClass() { @@ -55,10 +72,29 @@ export default { hasAuthor() { return this.author && Object.keys(this.author).length; }, - showGitlabTeamMemberBadge() { - return this.author?.is_gitlab_employee; + authorLinkClasses() { + return { + hover: this.isUsernameLinkHovered, + 'text-underline': this.isUsernameLinkHovered, + 'author-name-link': true, + 'js-user-link': true, + }; + }, + authorStatus() { + return this.author.status_tooltip_html; + }, + emojiElement() { + return this.$refs?.authorStatus?.querySelector('gl-emoji'); }, }, + 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() { @@ -69,6 +105,20 @@ 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; + }, + handleUsernameMouseLeave() { + this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave')); + this.isUsernameLinkHovered = false; + }, }, }; </script> @@ -87,18 +137,34 @@ export default { </div> <template v-if="hasAuthor"> <a - v-once + ref="authorNameLink" :href="author.path" - class="js-user-link" + :class="authorLinkClasses" :data-user-id="author.id" :data-username="author.username" > <slot name="note-header-info"></slot> <span class="note-header-author-name bold">{{ author.name }}</span> - <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> - <span class="note-headline-light">@{{ author.username }}</span> </a> - <gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" /> + <span + v-if="authorStatus" + ref="authorStatus" + v-on=" + authorStatusHasTooltip ? { mouseenter: removeEmojiTitle, mouseleave: addEmojiTitle } : {} + " + v-html="authorStatus" + ></span> + <span class="text-nowrap author-username"> + <a + ref="authorUsernameLink" + class="author-username-link" + :href="author.path" + @mouseenter="handleUsernameMouseEnter" + @mouseleave="handleUsernameMouseLeave" + ><span class="note-headline-light">@{{ author.username }}</span> + </a> + <gitlab-team-member-badge v-if="author && author.is_gitlab_employee" /> + </span> </template> <span v-else>{{ __('A deleted user') }}</span> <span class="note-headline-light note-headline-meta"> @@ -118,6 +184,15 @@ export default { </a> <time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" /> </template> + <gl-icon + v-if="isConfidential" + 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-800 align-middle" + /> <slot name="extra-controls"></slot> <i v-if="showSpinner" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index dea782683f2..37675e20b3d 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -255,10 +255,16 @@ export default { </div> <div class="timeline-content"> <div class="note-header"> - <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> + <note-header + v-once + :author="author" + :created-at="note.created_at" + :note-id="note.id" + :is-confidential="note.confidential" + > <slot slot="note-header-info" name="note-header-info"></slot> <span v-if="commit" v-html="actionText"></span> - <span v-else class="d-none d-sm-inline">·</span> + <span v-else-if="note.created_at" class="d-none d-sm-inline">·</span> </note-header> <note-actions :author-id="author.id" diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c1dd56aedf2..faa6006945d 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -230,10 +230,11 @@ export default { const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') }; if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) { - return Object.assign({}, defaultConfig, { + return { + ...defaultConfig, filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE, persistFilter: false, - }); + }; } return defaultConfig; }, |