summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/notes
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 14:34:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 14:34:42 +0000
commit9f46488805e86b1bc341ea1620b866016c2ce5ed (patch)
treef9748c7e287041e37d6da49e0a29c9511dc34768 /app/assets/javascripts/notes
parentdfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff)
downloadgitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'app/assets/javascripts/notes')
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue83
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue19
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue93
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue10
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue5
-rw-r--r--app/assets/javascripts/notes/index.js4
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js17
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/index.js6
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
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">&middot;</span>
+ <span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</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 });
},