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/components')
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue17
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue10
-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.vue65
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue72
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue112
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue103
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue7
18 files changed, 693 insertions, 79 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 5c67e429383..20dc7cb07e7 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -1,11 +1,12 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { n__ } from '~/locale';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { n__, __ } from '~/locale';
export default {
name: 'AssigneeTitle',
components: {
GlLoadingIcon,
+ GlIcon,
},
props: {
loading: {
@@ -26,12 +27,19 @@ export default {
required: false,
default: false,
},
+ changing: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
assigneeTitle() {
const assignees = this.numberOfAssignees;
return n__('Assignee', `%d Assignees`, assignees);
},
+ titleCopy() {
+ return this.changing ? __('Apply') : __('Edit');
+ },
},
};
</script>
@@ -43,11 +51,12 @@ export default {
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
+ data-test-id="edit-link"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
>
- {{ __('Edit') }}
+ {{ titleCopy }}
</a>
<a
v-if="showToggle"
@@ -56,7 +65,7 @@ export default {
href="#"
role="button"
>
- <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
+ <gl-icon aria-hidden="true" data-hidden="true" name="chevron-double-lg-right" :size="12" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 2f714ac3847..b9f268629fb 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -56,6 +56,9 @@ export default {
// Note: Realtime is only available on issues right now, future support for MR wil be built later.
return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue';
},
+ relativeUrlRoot() {
+ return gon.relative_url_root ?? '';
+ },
},
created() {
this.removeAssignee = this.store.removeAssignee.bind(this.store);
@@ -89,6 +92,8 @@ export default {
.saveAssignees(this.field)
.then(() => {
this.loading = false;
+ this.store.resetChanging();
+
refreshUserMergeRequestCounts();
})
.catch(() => {
@@ -113,10 +118,11 @@ export default {
:loading="loading || store.isFetching.assignees"
:editable="store.editable"
:show-toggle="!signedIn"
+ :changing="store.changing"
/>
<assignees
v-if="!store.isFetching.assignees"
- :root-path="store.rootPath"
+ :root-path="relativeUrlRoot"
:users="store.assignees"
:editable="store.editable"
:issuable-type="issuableType"
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..1af1bc18e3e 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -1,7 +1,6 @@
<script>
import $ from 'jquery';
import { difference, union } from 'lodash';
-import { mapState, mapActions } from 'vuex';
import flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -26,47 +25,49 @@ export default {
'projectIssuesPath',
'projectPath',
],
- data: () => ({
- labelsSelectInProgress: false,
- }),
- computed: {
- ...mapState(['selectedLabels']),
- },
- mounted() {
- this.setInitialState({
+ data() {
+ return {
+ isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
- });
+ };
},
methods: {
- ...mapActions(['setInitialState', 'replaceSelectedLabels']),
handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown');
},
- handleUpdateSelectedLabels(labels) {
+ handleUpdateSelectedLabels(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
- const userAddedLabelIds = labels.filter(label => label.set).map(label => label.id);
- const userRemovedLabelIds = labels.filter(label => !label.set).map(label => label.id);
+ const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id);
+ const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id);
- const issuableLabels = difference(
- union(currentLabelIds, userAddedLabelIds),
- userRemovedLabelIds,
- );
+ const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
- this.labelsSelectInProgress = true;
+ this.updateSelectedLabels(labelIds);
+ },
+ handleLabelRemove(labelId) {
+ const currentLabelIds = this.selectedLabels.map(label => label.id);
+ const labelIds = difference(currentLabelIds, [labelId]);
+
+ this.updateSelectedLabels(labelIds);
+ },
+ updateSelectedLabels(labelIds) {
+ this.isLabelsSelectInProgress = true;
axios({
data: {
[this.issuableType]: {
- label_ids: issuableLabels,
+ label_ids: labelIds,
},
},
method: 'put',
url: this.labelsUpdatePath,
})
- .then(({ data }) => this.replaceSelectedLabels(data.labels))
+ .then(({ data }) => {
+ this.selectedLabels = data.labels;
+ })
.catch(() => flash(__('An error occurred while updating labels.')))
.finally(() => {
- this.labelsSelectInProgress = false;
+ this.isLabelsSelectInProgress = false;
});
},
},
@@ -76,6 +77,7 @@ export default {
<template>
<labels-select
class="block labels js-labels-block"
+ :allow-label-remove="allowLabelEdit"
:allow-label-create="allowLabelCreate"
:allow-label-edit="allowLabelEdit"
:allow-multiselect="true"
@@ -86,10 +88,12 @@ export default {
:labels-fetch-path="labelsFetchPath"
:labels-filter-base-path="projectIssuesPath"
:labels-manage-path="labelsManagePath"
- :labels-select-in-progress="labelsSelectInProgress"
+ :labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.sidebar"
+ data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose"
+ @onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
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/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 53ee7f46ad9..b96a2b93712 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,8 +1,7 @@
<script>
import { mapGetters } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
@@ -26,7 +25,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -79,13 +78,9 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div
- v-tooltip
- :title="tooltipLabel"
+ v-gl-tooltip.left.viewport="{ title: tooltipLabel }"
class="sidebar-collapsed-icon"
data-testid="sidebar-collapse-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
@click="toggleForm"
>
<gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index e7dbc47aea1..c3a08f760a0 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,12 +1,11 @@
<script>
-import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
userAvatarImage,
@@ -87,12 +86,9 @@ export default {
<div>
<div
v-if="showParticipantLabel"
- v-tooltip
+ v-gl-tooltip.left.viewport
:title="participantLabel"
class="sidebar-collapsed-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
@click="onClickCollapsedIcon"
>
<gl-icon name="users" />
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..4f4f7002dc9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -0,0 +1,65 @@
+<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, GlIcon } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+export default {
+ name: 'ReviewerTitle',
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ },
+ 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"
+ >
+ <gl-icon aria-hidden="true" data-hidden="true" name="chevron-double-lg-right" :size="12" />
+ </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..aee94a55134
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -0,0 +1,112 @@
+<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,
+ };
+ },
+ computed: {
+ relativeUrlRoot() {
+ return gon.relative_url_root ?? '';
+ },
+ },
+ 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="relativeUrlRoot"
+ :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..e82a271d007
--- /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="gl-ml-3 gl-line-height-normal">
+ <div class="author">{{ user.name }}</div>
+ <div class="username">{{ username }}</div>
+ </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/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index bc2319c0f36..9d72bf4394e 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,7 +1,6 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
@@ -9,7 +8,7 @@ export default {
GlIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
showComparisonState: {
@@ -97,14 +96,7 @@ export default {
</script>
<template>
- <div
- v-tooltip
- :title="tooltipText"
- class="sidebar-collapsed-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- >
+ <div v-gl-tooltip:body.viewport.left :title="tooltipText" class="sidebar-collapsed-icon">
<gl-icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 4cb8d9ebd62..d4cc98e3743 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,7 +1,6 @@
<script>
-import { GlProgressBar } from '@gitlab/ui';
+import { GlProgressBar, GlTooltipDirective } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
-import tooltip from '../../../vue_shared/directives/tooltip';
import { s__, sprintf } from '~/locale';
export default {
@@ -10,7 +9,7 @@ export default {
GlProgressBar,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
timeSpent: {
@@ -73,7 +72,7 @@ export default {
<template>
<div class="time-tracking-comparison-pane">
<div
- v-tooltip
+ v-gl-tooltip
:title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
class="compare-meter"
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) {