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