Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/sidebar')
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue107
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue43
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue84
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue64
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue72
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue107
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue103
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue7
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js135
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js12
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js29
15 files changed, 767 insertions, 59 deletions
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 86bfacbfb9e..46d51138ccf 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -8,7 +8,7 @@ import eventHub from '../../event_hub';
export default {
components: {
- GlLoadingIcon,
+ GlButton,
},
props: {
fullPath: {
@@ -64,18 +64,18 @@ export default {
<template>
<div class="sidebar-item-warning-message-actions">
- <button type="button" class="btn btn-default gl-mr-3" @click="closeForm">
+ <gl-button class="gl-mr-3" @click="closeForm">
{{ __('Cancel') }}
- </button>
- <button
- type="button"
- class="btn btn-close"
- data-testid="confidential-toggle"
+ </gl-button>
+ <gl-button
+ category="secondary"
+ variant="warning"
:disabled="isLoading"
+ :loading="isLoading"
+ data-testid="confidential-toggle"
@click.prevent="submitForm"
>
- <gl-loading-icon v-if="isLoading" inline />
{{ toggleButtonText }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index d7be8927c29..0851ee21289 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -89,6 +89,7 @@ export default {
:labels-select-in-progress="labelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.sidebar"
+ data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index ea7230ae488..26a7c8e4a80 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __, sprintf } from '../../../locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -8,7 +8,7 @@ import eventHub from '../../event_hub';
export default {
components: {
- GlLoadingIcon,
+ GlButton,
},
inject: ['fullPath'],
props: {
@@ -65,19 +65,19 @@ export default {
<template>
<div class="sidebar-item-warning-message-actions">
- <button type="button" class="btn btn-default gl-mr-3" @click="closeForm">
+ <gl-button class="gl-mr-3" @click="closeForm">
{{ __('Cancel') }}
- </button>
+ </gl-button>
- <button
- type="button"
+ <gl-button
data-testid="lock-toggle"
- class="btn btn-close"
+ category="secondary"
+ variant="warning"
:disabled="isLoading"
+ :loading="isLoading"
@click.prevent="submitForm"
>
- <gl-loading-icon v-if="isLoading" inline />
{{ buttonText }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue
new file mode 100644
index 00000000000..6de926e0ff9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue
@@ -0,0 +1,24 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import ReviewerAvatar from './reviewer_avatar.vue';
+
+export default {
+ components: {
+ ReviewerAvatar,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <button type="button" class="btn-link">
+ <reviewer-avatar :user="user" :img-size="24" />
+ <span class="author"> {{ user.name }} </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
new file mode 100644
index 00000000000..45707c18f7b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
@@ -0,0 +1,107 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import CollapsedReviewer from './collapsed_reviewer.vue';
+
+const DEFAULT_MAX_COUNTER = 99;
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ CollapsedReviewer,
+ GlIcon,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ hasMoreThanOneReviewer() {
+ return this.users.length > 1;
+ },
+ hasMoreThanTwoReviewers() {
+ return this.users.length > 2;
+ },
+ allReviewersCanMerge() {
+ return this.users.every(user => user.can_merge);
+ },
+ sidebarAvatarCounter() {
+ if (this.users.length > DEFAULT_MAX_COUNTER) {
+ return `${DEFAULT_MAX_COUNTER}+`;
+ }
+
+ return `+${this.users.length - 1}`;
+ },
+ collapsedUsers() {
+ const collapsedLength = this.hasMoreThanTwoReviewers ? 1 : this.users.length;
+
+ return this.users.slice(0, collapsedLength);
+ },
+ tooltipTitleMergeStatus() {
+ const mergeLength = this.users.filter(u => u.can_merge).length;
+
+ if (mergeLength === this.users.length) {
+ return '';
+ } else if (mergeLength > 0) {
+ return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
+ mergeLength,
+ usersLength: this.users.length,
+ });
+ }
+
+ return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
+ },
+ tooltipTitle() {
+ const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
+ const renderUsers = this.users.slice(0, maxRender);
+ const names = renderUsers.map(u => u.name);
+
+ if (!this.users.length) {
+ return __('Reviewer(s)');
+ }
+
+ if (this.users.length > names.length) {
+ names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
+ }
+
+ const text = names.join(', ');
+
+ return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
+ },
+
+ tooltipOptions() {
+ return { container: 'body', placement: 'left', boundary: 'viewport' };
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="tooltipOptions"
+ :class="{ 'multiple-users': hasMoreThanOneReviewer }"
+ :title="tooltipTitle"
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
+ >
+ <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
+ <collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
+ <button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
+ <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
+ <i
+ v-if="!allReviewersCanMerge"
+ aria-hidden="true"
+ class="fa fa-exclamation-triangle merge-icon"
+ ></i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
new file mode 100644
index 00000000000..9fa3fa38eac
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
@@ -0,0 +1,43 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { __, sprintf } from '~/locale';
+
+export default {
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ imgSize: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ reviewerAlt() {
+ return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
+ },
+ avatarUrl() {
+ return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
+ },
+ hasMergeIcon() {
+ return !this.user.can_merge;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="position-relative">
+ <img
+ :alt="reviewerAlt"
+ :src="avatarUrl"
+ :width="imgSize"
+ :class="`s${imgSize}`"
+ class="avatar avatar-inline m-0"
+ data-qa-selector="avatar_image"
+ />
+ <i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
new file mode 100644
index 00000000000..b1b04564a62
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
@@ -0,0 +1,84 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import ReviewerAvatar from './reviewer_avatar.vue';
+
+export default {
+ components: {
+ ReviewerAvatar,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ default: 'bottom',
+ required: false,
+ },
+ tooltipHasName: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
+ issuableType: {
+ type: String,
+ default: 'issue',
+ required: false,
+ },
+ },
+ computed: {
+ cannotMerge() {
+ return this.issuableType === 'merge_request' && !this.user.can_merge;
+ },
+ tooltipTitle() {
+ if (this.cannotMerge && this.tooltipHasName) {
+ return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
+ } else if (this.cannotMerge) {
+ return __('Cannot merge');
+ } else if (this.tooltipHasName) {
+ return this.user.name;
+ }
+
+ return '';
+ },
+ tooltipOption() {
+ return {
+ container: 'body',
+ placement: this.tooltipPlacement,
+ boundary: 'viewport',
+ };
+ },
+ reviewerUrl() {
+ return this.user.web_url;
+ },
+ },
+};
+</script>
+
+<template>
+ <!-- must be `d-inline-block` or parent flex-basis causes width issues -->
+ <gl-link
+ v-gl-tooltip="tooltipOption"
+ :href="reviewerUrl"
+ :title="tooltipTitle"
+ class="d-inline-block"
+ >
+ <!-- use d-flex so that slot can be appropriately styled -->
+ <span class="d-flex">
+ <reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <slot :user="user"></slot>
+ </span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
new file mode 100644
index 00000000000..437f28907fd
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -0,0 +1,64 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { GlLoadingIcon } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+export default {
+ name: 'ReviewerTitle',
+ components: {
+ GlLoadingIcon,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ numberOfReviewers: {
+ type: Number,
+ required: true,
+ },
+ editable: {
+ type: Boolean,
+ required: true,
+ },
+ showToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ reviewerTitle() {
+ const reviewers = this.numberOfReviewers;
+ return n__('Reviewer', `%d Reviewers`, reviewers);
+ },
+ },
+};
+</script>
+<template>
+ <div class="title hide-collapsed">
+ {{ reviewerTitle }}
+ <gl-loading-icon v-if="loading" inline class="align-bottom" />
+ <a
+ v-if="editable"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
+ href="#"
+ data-track-event="click_edit_button"
+ data-track-label="right_sidebar"
+ data-track-property="reviewer"
+ >
+ {{ __('Edit') }}
+ </a>
+ <a
+ v-if="showToggle"
+ :aria-label="__('Toggle sidebar')"
+ class="gutter-toggle float-right js-sidebar-toggle"
+ href="#"
+ role="button"
+ >
+ <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
new file mode 100644
index 00000000000..6a3d88f6385
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -0,0 +1,72 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import CollapsedReviewerList from './collapsed_reviewer_list.vue';
+import UncollapsedReviewerList from './uncollapsed_reviewer_list.vue';
+
+export default {
+ // name: 'Reviewers' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ name: 'Reviewers',
+ components: {
+ CollapsedReviewerList,
+ UncollapsedReviewerList,
+ },
+ props: {
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ users: {
+ type: Array,
+ required: true,
+ },
+ editable: {
+ type: Boolean,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ sortedReviewers() {
+ const canMergeUsers = this.users.filter(user => user.can_merge);
+ const canNotMergeUsers = this.users.filter(user => !user.can_merge);
+
+ return [...canMergeUsers, ...canNotMergeUsers];
+ },
+ },
+ methods: {
+ assignSelf() {
+ this.$emit('assign-self');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
+
+ <div class="value hide-collapsed">
+ <template v-if="hasNoUsers">
+ <span class="assign-yourself no-value qa-assign-yourself">
+ {{ __('None') }}
+ </span>
+ </template>
+
+ <uncollapsed-reviewer-list
+ v-else
+ :users="sortedReviewers"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
new file mode 100644
index 00000000000..5d8a2e6fa65
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -0,0 +1,107 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import eventHub from '~/sidebar/event_hub';
+import Store from '~/sidebar/stores/sidebar_store';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ReviewerTitle from './reviewer_title.vue';
+import Reviewers from './reviewers.vue';
+import { __ } from '~/locale';
+
+export default {
+ name: 'SidebarReviewers',
+ components: {
+ ReviewerTitle,
+ Reviewers,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
+ },
+ field: {
+ type: String,
+ required: true,
+ },
+ signedIn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ issuableIid: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ store: new Store(),
+ loading: false,
+ };
+ },
+ created() {
+ this.removeReviewer = this.store.removeReviewer.bind(this.store);
+ this.addReviewer = this.store.addReviewer.bind(this.store);
+ this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store);
+
+ // Get events from deprecatedJQueryDropdown
+ eventHub.$on('sidebar.removeReviewer', this.removeReviewer);
+ eventHub.$on('sidebar.addReviewer', this.addReviewer);
+ eventHub.$on('sidebar.removeAllReviewers', this.removeAllReviewers);
+ eventHub.$on('sidebar.saveReviewers', this.saveReviewers);
+ },
+ beforeDestroy() {
+ eventHub.$off('sidebar.removeReviewer', this.removeReviewer);
+ eventHub.$off('sidebar.addReviewer', this.addReviewer);
+ eventHub.$off('sidebar.removeAllReviewers', this.removeAllReviewers);
+ eventHub.$off('sidebar.saveReviewers', this.saveReviewers);
+ },
+ methods: {
+ saveReviewers() {
+ this.loading = true;
+
+ this.mediator
+ .saveReviewers(this.field)
+ .then(() => {
+ this.loading = false;
+ // Uncomment once this issue has been addressed > https://gitlab.com/gitlab-org/gitlab/-/issues/237922
+ // refreshUserMergeRequestCounts();
+ })
+ .catch(() => {
+ this.loading = false;
+ return new Flash(__('Error occurred when saving reviewers'));
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <reviewer-title
+ :number-of-reviewers="store.reviewers.length"
+ :loading="loading || store.isFetching.reviewers"
+ :editable="store.editable"
+ :show-toggle="!signedIn"
+ />
+ <reviewers
+ v-if="!store.isFetching.reviewers"
+ :root-path="store.rootPath"
+ :users="store.reviewers"
+ :editable="store.editable"
+ :issuable-type="issuableType"
+ class="value"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
new file mode 100644
index 00000000000..2ae4a114b36
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -0,0 +1,103 @@
+<script>
+// NOTE! For the first iteration, we are simply copying the implementation of Assignees
+// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { __, sprintf } from '~/locale';
+import ReviewerAvatarLink from './reviewer_avatar_link.vue';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ components: {
+ ReviewerAvatarLink,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ data() {
+ return {
+ showLess: true,
+ };
+ },
+ computed: {
+ firstUser() {
+ return this.users[0];
+ },
+ hasOneUser() {
+ return this.users.length === 1;
+ },
+ hiddenReviewersLabel() {
+ const { numberOfHiddenReviewers } = this;
+ return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
+ },
+ renderShowMoreSection() {
+ return this.users.length > DEFAULT_RENDER_COUNT;
+ },
+ numberOfHiddenReviewers() {
+ return this.users.length - DEFAULT_RENDER_COUNT;
+ },
+ uncollapsedUsers() {
+ const uncollapsedLength = this.showLess
+ ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
+ : this.users.length;
+ return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
+ },
+ username() {
+ return `@${this.firstUser.username}`;
+ },
+ },
+ methods: {
+ toggleShowLess() {
+ this.showLess = !this.showLess;
+ },
+ },
+};
+</script>
+
+<template>
+ <reviewer-avatar-link
+ v-if="hasOneUser"
+ #default="{ user }"
+ tooltip-placement="left"
+ :tooltip-has-name="false"
+ :user="firstUser"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ >
+ <div class="ml-2">
+ <span class="author"> {{ user.name }} </span>
+ <span class="username"> {{ username }} </span>
+ </div>
+ </reviewer-avatar-link>
+ <div v-else>
+ <div class="user-list">
+ <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
+ <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
+ </div>
+ </div>
+ <div v-if="renderShowMoreSection" class="user-list-more">
+ <button
+ type="button"
+ class="btn-link"
+ data-qa-selector="more_reviewers_link"
+ @click="toggleShowLess"
+ >
+ <template v-if="showLess">
+ {{ hiddenReviewersLabel }}
+ </template>
+ <template v-else>{{ __('- show less') }}</template>
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 05ad7b4ea3e..406677941b7 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -26,11 +26,14 @@ export default {
methods: {
listenForQuickActions() {
$(document).on('ajax:success', '.gfm-form', this.quickActionListened);
+
eventHub.$on('timeTrackingUpdated', data => {
- this.quickActionListened(null, data);
+ this.quickActionListened({ detail: [data] });
});
},
- quickActionListened(e, data) {
+ quickActionListened(e) {
+ const data = e.detail[0];
+
const subscribedCommands = ['spend_time', 'time_estimate'];
let changedCommands;
if (data !== undefined) {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index be559b16420..a25a7b0b2fe 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue';
+import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
@@ -13,17 +14,17 @@ import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptio
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
-import { store } from '~/notes/stores';
-import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
-import mergeRequestStore from '~/mr_notes/stores';
+import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
Vue.use(Translate);
Vue.use(VueApollo);
Vue.use(Vuex);
-function getSidebarOptions() {
- return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-options')) {
+ return JSON.parse(sidebarOptEl.innerHTML);
}
function mountAssigneesComponent(mediator) {
@@ -50,6 +51,36 @@ function mountAssigneesComponent(mediator) {
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: isInIssuePage() || isInIncidentPage() ? 'issue' : 'merge_request',
+ },
+ }),
+ });
+}
+
+function mountReviewersComponent(mediator) {
+ const el = document.getElementById('js-vue-sidebar-reviewers');
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ if (!el) return;
+
+ const { iid, fullPath } = getSidebarOptions();
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ apolloProvider,
+ components: {
+ SidebarReviewers,
+ },
+ render: createElement =>
+ createElement('sidebar-reviewers', {
+ props: {
+ mediator,
+ issuableIid: String(iid),
+ projectPath: fullPath,
+ field: el.dataset.field,
+ signedIn: el.hasAttribute('data-signed-in'),
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
},
}),
@@ -89,47 +120,74 @@ function mountConfidentialComponent(mediator) {
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
- // eslint-disable-next-line no-new
- new Vue({
- el,
- store,
- components: {
- ConfidentialIssueSidebar,
- },
- render: createElement =>
- createElement('confidential-issue-sidebar', {
- props: {
- iid: String(iid),
- fullPath,
- isEditable: initialData.is_editable,
- service: mediator.service,
- },
- }),
- });
+ import(/* webpackChunkName: 'notesStore' */ '~/notes/stores')
+ .then(
+ ({ store }) =>
+ new Vue({
+ el,
+ store,
+ components: {
+ ConfidentialIssueSidebar,
+ },
+ render: createElement =>
+ createElement('confidential-issue-sidebar', {
+ props: {
+ iid: String(iid),
+ fullPath,
+ isEditable: initialData.is_editable,
+ service: mediator.service,
+ },
+ }),
+ }),
+ )
+ .catch(() => {
+ createFlash({ message: __('Failed to load sidebar confidential toggle') });
+ });
}
function mountLockComponent() {
const el = document.getElementById('js-lock-entry-point');
+
+ if (!el) {
+ return;
+ }
+
const { fullPath } = getSidebarOptions();
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
- return el
- ? new Vue({
- el,
- store: isInIssuePage() ? store : mergeRequestStore,
- provide: {
- fullPath,
- },
- render: createElement =>
- createElement(IssuableLockForm, {
- props: {
- isEditable: initialData.is_editable,
- },
- }),
- })
- : undefined;
+ let importStore;
+ if (isInIssuePage() || isInIncidentPage()) {
+ importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then(
+ ({ store }) => store,
+ );
+ } else {
+ importStore = import(/* webpackChunkName: 'mrNotesStore' */ '~/mr_notes/stores').then(
+ store => store.default,
+ );
+ }
+
+ importStore
+ .then(
+ store =>
+ new Vue({
+ el,
+ store,
+ provide: {
+ fullPath,
+ },
+ render: createElement =>
+ createElement(IssuableLockForm, {
+ props: {
+ isEditable: initialData.is_editable,
+ },
+ }),
+ }),
+ )
+ .catch(() => {
+ createFlash({ message: __('Failed to load sidebar lock status') });
+ });
}
function mountParticipantsComponent(mediator) {
@@ -218,8 +276,9 @@ function mountSeverityComponent() {
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
+ mountReviewersComponent(mediator);
mountConfidentialComponent(mediator);
- mountLockComponent(mediator);
+ mountLockComponent();
mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator);
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 8f1f76a2e02..2146fb83b13 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -40,6 +40,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
+ saveReviewers(field) {
+ const selected = this.store.reviewers.map(u => u.id);
+
+ // If there are no ids, that means we have to unassign (which is id = 0)
+ // And it only accepts an array, hence [0]
+ const reviewers = selected.length === 0 ? [0] : selected;
+ const data = { reviewer_ids: reviewers };
+
+ return this.service.update(field, data);
+ }
+
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
@@ -55,6 +66,7 @@ export default class SidebarMediator {
processFetchedData(data) {
this.store.setAssigneeData(data);
+ this.store.setReviewerData(data);
this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 095f93b72a9..8d0d093e920 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -18,8 +18,10 @@ export default class SidebarStore {
this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = [];
+ this.reviewers = [];
this.isFetching = {
assignees: true,
+ reviewers: true,
participants: true,
subscriptions: true,
};
@@ -42,6 +44,13 @@ export default class SidebarStore {
}
}
+ setReviewerData(data) {
+ this.isFetching.reviewers = false;
+ if (data.reviewers) {
+ this.reviewers = data.reviewers;
+ }
+ }
+
setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent;
@@ -75,20 +84,40 @@ export default class SidebarStore {
}
}
+ addReviewer(reviewer) {
+ if (!this.findReviewer(reviewer)) {
+ this.reviewers.push(reviewer);
+ }
+ }
+
findAssignee(findAssignee) {
return this.assignees.find(assignee => assignee.id === findAssignee.id);
}
+ findReviewer(findReviewer) {
+ return this.reviewers.find(reviewer => reviewer.id === findReviewer.id);
+ }
+
removeAssignee(removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
+ removeReviewer(removeReviewer) {
+ if (removeReviewer) {
+ this.reviewers = this.reviewers.filter(reviewer => reviewer.id !== removeReviewer.id);
+ }
+ }
+
removeAllAssignees() {
this.assignees = [];
}
+ removeAllReviewers() {
+ this.reviewers = [];
+ }
+
setAssigneesFromRealtime(data) {
this.assignees = data;
}