diff options
Diffstat (limited to 'app/assets/javascripts/sidebar/components')
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) { |