diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /app/assets/javascripts/notes | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) | |
download | gitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'app/assets/javascripts/notes')
14 files changed, 205 insertions, 49 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; }, diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 8f9e2359e0d..ba814649078 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -2,11 +2,9 @@ import Vue from 'vue'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; import initSortDiscussions from './sort_discussions'; -import createStore from './stores'; +import { store } from './stores'; document.addEventListener('DOMContentLoaded', () => { - const store = createStore(); - // eslint-disable-next-line no-new new Vue({ el: '#js-vue-notes', diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index 08c7efd69a6..c9026352d18 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -1,6 +1,6 @@ import { mapGetters, mapActions, mapState } from 'vuex'; import { scrollToElement } from '~/lib/utils/common_utils'; -import eventHub from '../../notes/event_hub'; +import eventHub from '../event_hub'; /** * @param {string} selector diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 1b80b59621a..0999d0aa7ac 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -185,12 +185,27 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, }); }; +export const toggleBlockedIssueWarning = ({ commit }, value) => { + commit(types.TOGGLE_BLOCKED_ISSUE_WARNING, value); + // Hides Close issue button at the top of issue page + const closeDropdown = document.querySelector('.js-issuable-close-dropdown'); + if (closeDropdown) { + closeDropdown.classList.toggle('d-none'); + } else { + const closeButton = document.querySelector( + '.detail-page-header-actions .btn-close.btn-grouped', + ); + closeButton.classList.toggle('d-md-block'); + } +}; + export const closeIssue = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); return axios.put(state.notesData.closePath).then(({ data }) => { commit(types.CLOSE_ISSUE); dispatch('emitStateChangedEvent', data); dispatch('toggleStateButtonLoading', false); + dispatch('toggleBlockedIssueWarning', false); }); }; @@ -233,7 +248,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const hasQuickActions = utils.hasQuickActions(placeholderText); const replyId = noteData.data.in_reply_to_discussion_id; let methodToDispatch; - const postData = Object.assign({}, noteData); + const postData = { ...noteData }; if (postData.isDraft === true) { methodToDispatch = replyId ? 'batchComments/addDraftToDiscussion' diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index eb877083bca..85997b44bcc 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -35,6 +35,8 @@ export const getNoteableData = state => state.noteableData; export const getNoteableDataByProp = state => prop => state.noteableData[prop]; +export const getBlockedByIssues = state => state.noteableData.blocked_by_issues; + export const userCanReply = state => Boolean(state.noteableData.current_user.can_create_note); export const openState = state => state.noteableData.state; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index d41b02b4a4b..c4895f58656 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -4,4 +4,8 @@ import notesModule from './modules'; Vue.use(Vuex); -export default () => new Vuex.Store(notesModule()); +// NOTE: Giving the option to either use a singleton or new instance of notes. +const notesStore = () => new Vuex.Store(notesModule()); + +export default notesStore; +export const store = notesStore(); diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 81844ad6e98..25f0f546103 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -14,6 +14,7 @@ export default () => ({ // View layer isToggleStateButtonLoading: false, + isToggleBlockedIssueWarning: false, isNotesFetched: false, isLoading: true, isLoadingDescriptionVersion: false, @@ -24,6 +25,7 @@ export default () => ({ }, userData: {}, noteableData: { + confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes. current_user: {}, preview_note_path: 'path/to/preview', }, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 5b7225bb3d2..2f7b2788d8a 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -33,6 +33,7 @@ export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT'; export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; +export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING'; // Description version export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index dab09d1d05c..f06874991f0 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -249,6 +249,10 @@ export default { Object.assign(state, { isToggleStateButtonLoading: value }); }, + [types.TOGGLE_BLOCKED_ISSUE_WARNING](state, value) { + Object.assign(state, { isToggleBlockedIssueWarning: value }); + }, + [types.SET_NOTES_FETCHED_STATE](state, value) { Object.assign(state, { isNotesFetched: value }); }, |