Commit 61d86343 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '300658-refactor-sidebar-notifications-subscriptions-component-to-use-vue-apollo' into 'master'

Sidebar notifications subscriptions widget [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!60345
parents 3d86a9d3 4f584519
...@@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils'; import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
BoardSidebarTimeTracker, BoardSidebarTimeTracker,
BoardSidebarLabelsSelect, BoardSidebarLabelsSelect,
BoardSidebarDueDate, BoardSidebarDueDate,
BoardSidebarSubscription, SidebarSubscriptionsWidget,
BoardSidebarMilestoneSelect, BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect: () => BoardSidebarEpicSelect: () =>
import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'), import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
...@@ -98,7 +98,12 @@ export default { ...@@ -98,7 +98,12 @@ export default {
:issuable-type="issuableType" :issuable-type="issuableType"
@confidentialityUpdated="setActiveItemConfidential($event)" @confidentialityUpdated="setActiveItemConfidential($event)"
/> />
<board-sidebar-subscription class="subscriptions" /> <sidebar-subscriptions-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
data-testid="sidebar-notifications"
/>
</template> </template>
</gl-drawer> </gl-drawer>
</template> </template>
...@@ -43,6 +43,11 @@ export default { ...@@ -43,6 +43,11 @@ export default {
property: null, property: null,
}), }),
}, },
canEdit: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -113,8 +118,9 @@ export default { ...@@ -113,8 +118,9 @@ export default {
inline inline
class="gl-mx-auto gl-my-0 hide-expanded" class="gl-mx-auto gl-my-0 hide-expanded"
/> />
<slot name="collapsed-right"></slot>
<gl-button <gl-button
v-if="canUpdate && !initialLoading" v-if="canUpdate && !initialLoading && canEdit"
variant="link" variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed" class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid="edit-button" data-testid="edit-button"
......
<script>
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __ } from '../../../locale';
import Store from '../../stores/sidebar_store';
import subscriptions from './subscriptions.vue';
export default {
components: {
subscriptions,
},
props: {
mediator: {
type: Object,
required: true,
},
},
data() {
return {
store: new Store(),
};
},
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription().catch(() => {
Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
};
</script>
<template>
<div class="block subscriptions">
<subscriptions
:loading="store.isFetching.subscriptions"
:project-emails-disabled="store.projectEmailsDisabled"
:subscribe-disabled-description="store.subscribeDisabledDescription"
:subscribed="store.subscribed"
@toggleSubscription="onToggleSubscription"
/>
</div>
</template>
<script>
import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { subscribedQueries } from '~/sidebar/constants';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
export default {
tracking: {
event: 'click_edit_button',
label: 'right_sidebar',
property: 'subscriptions',
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
GlLoadingIcon,
GlToggle,
SidebarEditableItem,
},
inject: ['canUpdate'],
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
subscribed: false,
loading: false,
emailsDisabled: false,
};
},
apollo: {
subscribed: {
query() {
return subscribedQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: String(this.iid),
};
},
update(data) {
return data.workspace?.issuable?.subscribed || false;
},
result({ data }) {
this.emailsDisabled = this.parentIsGroup
? data.workspace?.emailsDisabled
: data.workspace?.issuable?.emailsDisabled;
this.$emit('subscribedUpdated', data.workspace?.issuable?.subscribed);
},
error() {
createFlash({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
issuableType: this.issuableType,
},
),
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries?.subscribed?.loading || this.loading;
},
notificationTooltip() {
if (this.emailsDisabled) {
return this.subscribeDisabledDescription;
}
return this.subscribed ? this.$options.i18n.labelOn : this.$options.i18n.labelOff;
},
notificationIcon() {
if (this.emailsDisabled || !this.subscribed) {
return ICON_OFF;
}
return ICON_ON;
},
parentIsGroup() {
return this.issuableType === IssuableType.Epic;
},
subscribeDisabledDescription() {
return sprintf(__('Disabled by %{parent} owner'), {
parent: this.parentIsGroup ? 'group' : 'project',
});
},
},
methods: {
setSubscribed(subscribed) {
this.loading = true;
this.$apollo
.mutate({
mutation: subscribedQueries[this.issuableType].mutation,
variables: {
fullPath: this.fullPath,
iid: this.iid,
subscribedState: subscribed,
},
})
.then(
({
data: {
updateIssuableSubscription: { errors },
},
}) => {
if (errors.length) {
createFlash({
message: errors[0],
});
}
},
)
.catch(() => {
createFlash({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
issuableType: this.issuableType,
},
),
});
})
.finally(() => {
this.loading = false;
});
},
toggleSubscribed() {
if (this.emailsDisabled) {
this.expandSidebar();
} else {
this.setSubscribed(!this.subscribed);
}
},
expandSidebar() {
this.$emit('expandSidebar');
},
},
i18n: {
notifications: __('Notifications'),
labelOn: __('Notifications on'),
labelOff: __('Notifications off'),
},
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="$options.i18n.notifications"
:tracking="$options.tracking"
:loading="isLoading"
:can-edit="false"
class="block subscriptions"
>
<template #collapsed-right>
<gl-toggle
:value="subscribed"
:is-loading="isLoading"
:disabled="emailsDisabled || !canUpdate"
class="hide-collapsed gl-ml-auto"
data-testid="subscription-toggle"
:label="$options.i18n.notifications"
label-position="hidden"
@change="setSubscribed"
/>
</template>
<template #collapsed>
<span
ref="tooltip"
v-gl-tooltip.viewport.left
:title="notificationTooltip"
class="sidebar-collapsed-icon"
@click="toggleSubscribed"
>
<gl-loading-icon v-if="isLoading" class="sidebar-item-icon is-active" />
<gl-icon v-else :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" />
</span>
<div v-show="emailsDisabled" class="gl-mt-3 hide-collapsed gl-text-gray-500">
{{ subscribeDisabledDescription }}
</div>
</template>
<template #default> </template>
</sidebar-editable-item>
</template>
...@@ -2,16 +2,22 @@ import { IssuableType } from '~/issue_show/constants'; ...@@ -2,16 +2,22 @@ import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql'; import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql'; import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql'; import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql'; import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql'; import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql'; import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql';
...@@ -80,6 +86,21 @@ export const dateFields = { ...@@ -80,6 +86,21 @@ export const dateFields = {
}, },
}; };
export const subscribedQueries = {
[IssuableType.Issue]: {
query: issueSubscribedQuery,
mutation: updateIssueSubscriptionMutation,
},
[IssuableType.Epic]: {
query: epicSubscribedQuery,
mutation: updateEpicSubscriptionMutation,
},
[IssuableType.MergeRequest]: {
query: mergeRequestSubscribed,
mutation: updateMergeRequestSubscriptionMutation,
},
};
export const dueDateQueries = { export const dueDateQueries = {
[IssuableType.Issue]: { [IssuableType.Issue]: {
query: issueDueDateQuery, query: issueDueDateQuery,
......
...@@ -24,7 +24,7 @@ import IssuableLockForm from './components/lock/issuable_lock_form.vue'; ...@@ -24,7 +24,7 @@ import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue'; import sidebarParticipants from './components/participants/sidebar_participants.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue'; import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue'; import SidebarSeverity from './components/severity/sidebar_severity.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
...@@ -334,21 +334,32 @@ function mountParticipantsComponent(mediator) { ...@@ -334,21 +334,32 @@ function mountParticipantsComponent(mediator) {
}); });
} }
function mountSubscriptionsComponent(mediator) { function mountSubscriptionsComponent() {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return; if (!el) return;
const { fullPath, iid, editable } = getSidebarOptions();
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
apolloProvider,
components: { components: {
sidebarSubscriptions, SidebarSubscriptionsWidget,
},
provide: {
canUpdate: editable,
}, },
render: (createElement) => render: (createElement) =>
createElement('sidebar-subscriptions', { createElement('sidebar-subscriptions-widget', {
props: { props: {
mediator, iid: String(iid),
fullPath,
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
}, },
}), }),
}); });
...@@ -425,7 +436,7 @@ export function mountSidebar(mediator) { ...@@ -425,7 +436,7 @@ export function mountSidebar(mediator) {
mountReferenceComponent(mediator); mountReferenceComponent(mediator);
mountLockComponent(); mountLockComponent();
mountParticipantsComponent(mediator); mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator); mountSubscriptionsComponent();
mountCopyEmailComponent(); mountCopyEmailComponent();
new SidebarMoveIssue( new SidebarMoveIssue(
......
query epicSubscribed($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
emailsDisabled
issuable: epic(iid: $iid) {
__typename
id
subscribed
}
}
}
query issueSubscribed($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
subscribed
emailsDisabled
}
}
}
query mergeRequestSubscribed($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: mergeRequest(iid: $iid) {
__typename
id
subscribed
}
}
}
mutation epicSetSubscription($input: EpicSetSubscriptionInput!) { mutation epicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
updateIssuableSubscription: epicSetSubscription(input: $input) { updateIssuableSubscription: epicSetSubscription(
epic { input: { groupPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
issuable: epic {
id
subscribed subscribed
} }
errors errors
......
mutation issueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
updateIssuableSubscription: issueSetSubscription(
input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
issuable: issue {
id
subscribed
}
errors
}
}
mutation mergeRequestSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
updateIssuableSubscription: mergeRequestSetSubscription(
input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
issuable: mergeRequest {
id
subscribed
}
errors
}
}
---
title: Toggle subscribed state when clicking on icon in collapsed sidebar
merge_request: 60345
author:
type: changed
...@@ -2,26 +2,26 @@ ...@@ -2,26 +2,26 @@
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils'; import { contentTop } from '~/lib/utils/common_utils';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default { export default {
headerHeight: `${contentTop()}px`, headerHeight: `${contentTop()}px`,
components: { components: {
GlDrawer, GlDrawer,
BoardSidebarLabelsSelect, BoardSidebarLabelsSelect,
BoardSidebarSubscription,
BoardSidebarTitle, BoardSidebarTitle,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
SidebarDateWidget, SidebarDateWidget,
SidebarSubscriptionsWidget,
}, },
computed: { computed: {
...mapGetters(['isSidebarOpen', 'activeBoardItem']), ...mapGetters(['isSidebarOpen', 'activeBoardItem']),
...mapState(['sidebarType', 'fullPath']), ...mapState(['sidebarType', 'fullPath', 'issuableType']),
isIssuableSidebar() { isIssuableSidebar() {
return this.sidebarType === ISSUABLE; return this.sidebarType === ISSUABLE;
}, },
...@@ -30,7 +30,7 @@ export default { ...@@ -30,7 +30,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleBoardItem', 'setActiveItemConfidential']), ...mapActions(['toggleBoardItem', 'setActiveItemConfidential', 'setActiveItemSubscribed']),
handleClose() { handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType }); this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
}, },
...@@ -52,24 +52,28 @@ export default { ...@@ -52,24 +52,28 @@ export default {
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
:full-path="fullPath" :full-path="fullPath"
date-type="startDate" date-type="startDate"
issuable-type="epic" :issuable-type="issuableType"
:can-inherit="true" :can-inherit="true"
/> />
<sidebar-date-widget <sidebar-date-widget
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
:full-path="fullPath" :full-path="fullPath"
date-type="dueDate" date-type="dueDate"
issuable-type="epic" :issuable-type="issuableType"
:can-inherit="true" :can-inherit="true"
/> />
<board-sidebar-labels-select class="labels" /> <board-sidebar-labels-select class="labels" />
<sidebar-confidentiality-widget <sidebar-confidentiality-widget
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
:full-path="fullPath" :full-path="fullPath"
issuable-type="epic" :issuable-type="issuableType"
@confidentialityUpdated="setActiveItemConfidential($event)" @confidentialityUpdated="setActiveItemConfidential($event)"
/> />
<board-sidebar-subscription class="subscriptions" /> <sidebar-subscriptions-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
/>
</template> </template>
</gl-drawer> </gl-drawer>
</template> </template>
...@@ -3,9 +3,11 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -3,9 +3,11 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue'; import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
import { IssuableType } from '~/issue_show/constants';
import notesEventHub from '~/notes/event_hub'; import notesEventHub from '~/notes/event_hub';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarParticipants from '~/sidebar/components/participants/participants.vue'; import SidebarParticipants from '~/sidebar/components/participants/participants.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import sidebarEventHub from '~/sidebar/event_hub'; import sidebarEventHub from '~/sidebar/event_hub';
import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
...@@ -14,7 +16,6 @@ import epicUtils from '../utils/epic_utils'; ...@@ -14,7 +16,6 @@ import epicUtils from '../utils/epic_utils';
import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue'; import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue';
import SidebarHeader from './sidebar_items/sidebar_header.vue'; import SidebarHeader from './sidebar_items/sidebar_header.vue';
import SidebarLabels from './sidebar_items/sidebar_labels.vue'; import SidebarLabels from './sidebar_items/sidebar_labels.vue';
import SidebarSubscription from './sidebar_items/sidebar_subscription.vue';
import SidebarTodo from './sidebar_items/sidebar_todo.vue'; import SidebarTodo from './sidebar_items/sidebar_todo.vue';
export default { export default {
...@@ -27,8 +28,8 @@ export default { ...@@ -27,8 +28,8 @@ export default {
SidebarLabels, SidebarLabels,
AncestorsTree, AncestorsTree,
SidebarParticipants, SidebarParticipants,
SidebarSubscription,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
SidebarSubscriptionsWidget,
}, },
inject: ['iid'], inject: ['iid'],
data() { data() {
...@@ -69,6 +70,9 @@ export default { ...@@ -69,6 +70,9 @@ export default {
'dueDateForCollapsedSidebar', 'dueDateForCollapsedSidebar',
'ancestors', 'ancestors',
]), ]),
issuableType() {
return IssuableType.Epic;
},
}, },
mounted() { mounted() {
this.toggleSidebarFlag(epicUtils.getCollapsedGutter()); this.toggleSidebarFlag(epicUtils.getCollapsedGutter());
...@@ -225,7 +229,7 @@ export default { ...@@ -225,7 +229,7 @@ export default {
<sidebar-confidentiality-widget <sidebar-confidentiality-widget
:iid="String(iid)" :iid="String(iid)"
:full-path="fullPath" :full-path="fullPath"
issuable-type="epic" :issuable-type="issuableType"
@closeForm="handleSidebarToggle" @closeForm="handleSidebarToggle"
@expandSidebar="handleSidebarToggle" @expandSidebar="handleSidebarToggle"
@confidentialityUpdated="updateConfidentialityOnIssuable($event)" @confidentialityUpdated="updateConfidentialityOnIssuable($event)"
...@@ -239,7 +243,13 @@ export default { ...@@ -239,7 +243,13 @@ export default {
@toggleSidebar="toggleSidebar({ sidebarCollapsed })" @toggleSidebar="toggleSidebar({ sidebarCollapsed })"
/> />
</div> </div>
<sidebar-subscription :sidebar-collapsed="sidebarCollapsed" data-testid="subscribe" /> <sidebar-subscriptions-widget
:iid="String(iid)"
:full-path="fullPath"
:issuable-type="issuableType"
data-testid="subscribe"
@expandSidebar="handleSidebarToggle"
/>
</div> </div>
</aside> </aside>
</template> </template>
<script>
import { mapState, mapActions } from 'vuex';
import Subscription from '~/sidebar/components/subscriptions/subscriptions.vue';
export default {
components: {
Subscription,
},
props: {
sidebarCollapsed: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['subscribed', 'epicSubscriptionToggleInProgress']),
},
methods: {
...mapActions(['toggleSidebar', 'toggleEpicSubscription']),
},
};
</script>
<template>
<div class="block subscription">
<subscription
:loading="epicSubscriptionToggleInProgress"
:subscribed="subscribed"
@toggleSubscription="toggleEpicSubscription"
@toggleSidebar="toggleSidebar({ sidebarCollapsed })"
/>
</div>
</template>
...@@ -62,7 +62,7 @@ export default () => { ...@@ -62,7 +62,7 @@ export default () => {
groupId: parseInt($boardApp.dataset.groupId, 10), groupId: parseInt($boardApp.dataset.groupId, 10),
rootPath: $boardApp.dataset.rootPath, rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null, currentUserId: gon.current_user_id || null,
canUpdate: $boardApp.dataset.canUpdate, canUpdate: parseBoolean($boardApp.dataset.canUpdate),
canAdminList: parseBoolean($boardApp.dataset.canAdminList), canAdminList: parseBoolean($boardApp.dataset.canAdminList),
labelsFetchPath: $boardApp.dataset.labelsFetchPath, labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath, labelsManagePath: $boardApp.dataset.labelsManagePath,
......
...@@ -176,21 +176,27 @@ RSpec.describe 'Epic boards sidebar', :js do ...@@ -176,21 +176,27 @@ RSpec.describe 'Epic boards sidebar', :js do
it 'displays notifications toggle', :aggregate_failures do it 'displays notifications toggle', :aggregate_failures do
click_card(card) click_card(card)
page.within('[data-testid="sidebar-notifications"]') do page.within('.subscriptions') do
expect(page).to have_button('Notifications') expect(page).to have_button('Notifications')
expect(page).not_to have_content('Notifications have been disabled by the project or group owner') expect(page).not_to have_content('Disabled by group owner')
end end
end end
it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/329292' do it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do
click_card(card) click_card(card)
wait_for_requests
click_button 'Notifications' click_button 'Notifications'
wait_for_requests
expect(page).to have_button('Notifications', class: 'is-checked') expect(page).to have_button('Notifications', class: 'is-checked')
click_button 'Notifications' click_button 'Notifications'
wait_for_requests
expect(page).not_to have_button('Notifications', class: 'is-checked') expect(page).not_to have_button('Notifications', class: 'is-checked')
end end
...@@ -202,9 +208,9 @@ RSpec.describe 'Epic boards sidebar', :js do ...@@ -202,9 +208,9 @@ RSpec.describe 'Epic boards sidebar', :js do
end end
it 'displays a message that notifications have been disabled' do it 'displays a message that notifications have been disabled' do
page.within('[data-testid="sidebar-notifications"]') do page.within('.subscriptions') do
expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]') expect(page).to have_button('Notifications', class: 'is-disabled')
expect(page).to have_content('Notifications have been disabled by the project or group owner') expect(page).to have_content('Disabled by group owner')
end end
end end
end end
......
...@@ -6,6 +6,7 @@ import { stubComponent } from 'helpers/stub_component'; ...@@ -6,6 +6,7 @@ import { stubComponent } from 'helpers/stub_component';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockEpic } from '../mock_data'; import { mockEpic } from '../mock_data';
describe('EpicBoardContentSidebar', () => { describe('EpicBoardContentSidebar', () => {
...@@ -80,6 +81,10 @@ describe('EpicBoardContentSidebar', () => { ...@@ -80,6 +81,10 @@ describe('EpicBoardContentSidebar', () => {
expect(wrapper.find(SidebarConfidentialityWidget).exists()).toBe(true); expect(wrapper.find(SidebarConfidentialityWidget).exists()).toBe(true);
}); });
it('renders SidebarSubscriptionsWidget', () => {
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
});
describe('when we emit close', () => { describe('when we emit close', () => {
let toggleBoardItem; let toggleBoardItem;
......
...@@ -10,6 +10,8 @@ import epicUtils from 'ee/epic/utils/epic_utils'; ...@@ -10,6 +10,8 @@ import epicUtils from 'ee/epic/utils/epic_utils';
import { parsePikadayDate } from '~/lib/utils/datetime_utility'; import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockEpicMeta, mockEpicData, mockAncestors } from '../mock_data'; import { mockEpicMeta, mockEpicData, mockAncestors } from '../mock_data';
describe('EpicSidebarComponent', () => { describe('EpicSidebarComponent', () => {
...@@ -207,6 +209,10 @@ describe('EpicSidebarComponent', () => { ...@@ -207,6 +209,10 @@ describe('EpicSidebarComponent', () => {
expect(wrapper.find('[data-testid="labels-select"]').exists()).toBe(true); expect(wrapper.find('[data-testid="labels-select"]').exists()).toBe(true);
}); });
it('renders SidebarSubscriptionsWidget', () => {
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
});
describe('when sub-epics feature is available', () => { describe('when sub-epics feature is available', () => {
it('renders ancestors list', async () => { it('renders ancestors list', async () => {
store.dispatch('toggleSidebarFlag', false); store.dispatch('toggleSidebarFlag', false);
......
import { GlToggle } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import SidebarSubscription from 'ee/epic/components/sidebar_items/sidebar_subscription.vue';
import createStore from 'ee/epic/store';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('SidebarSubscriptionComponent', () => {
let wrapper;
beforeEach(() => {
wrapper = extendedWrapper(
mount(SidebarSubscription, {
store: createStore(),
propsData: { sidebarCollapsed: false },
}),
);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('template', () => {
it('renders subscription toggle element container', () => {
expect(wrapper.classes('block')).toBe(true);
expect(wrapper.classes('subscription')).toBe(true);
});
it('renders toggle title text', () => {
expect(wrapper.findByTestId('subscription-title').text()).toBe('Notifications');
});
it('renders toggle button element', () => {
expect(wrapper.findComponent(GlToggle).exists()).toBe(true);
});
});
});
...@@ -11424,6 +11424,9 @@ msgstr "" ...@@ -11424,6 +11424,9 @@ msgstr ""
msgid "Disabled" msgid "Disabled"
msgstr "" msgstr ""
msgid "Disabled by %{parent} owner"
msgstr ""
msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them." msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
msgstr "" msgstr ""
...@@ -12830,9 +12833,6 @@ msgstr "" ...@@ -12830,9 +12833,6 @@ msgstr ""
msgid "Error occurred when saving reviewers" msgid "Error occurred when saving reviewers"
msgstr "" msgstr ""
msgid "Error occurred when toggling the notification subscription"
msgstr ""
msgid "Error occurred while updating the issue status" msgid "Error occurred while updating the issue status"
msgstr "" msgstr ""
...@@ -30044,6 +30044,9 @@ msgstr "" ...@@ -30044,6 +30044,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality." msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr "" msgstr ""
msgid "Something went wrong while setting %{issuableType} notifications."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again." msgid "Something went wrong while stopping this environment. Please try again."
msgstr "" msgstr ""
......
...@@ -32,8 +32,8 @@ RSpec.describe "User toggles subscription", :js do ...@@ -32,8 +32,8 @@ RSpec.describe "User toggles subscription", :js do
let(:project) { create(:project_empty_repo, :public, emails_disabled: true) } let(:project) { create(:project_empty_repo, :public, emails_disabled: true) }
it 'is disabled' do it 'is disabled' do
expect(page).to have_content('Notifications have been disabled by the project or group owner') expect(page).to have_content('Disabled by project owner')
expect(page).not_to have_selector('[data-testid="subscription-toggle"]') expect(page).to have_button('Notifications', class: 'is-disabled')
end end
end end
end end
...@@ -6,9 +6,9 @@ import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; ...@@ -6,9 +6,9 @@ import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
describe('BoardContentSidebar', () => { describe('BoardContentSidebar', () => {
...@@ -111,7 +111,7 @@ describe('BoardContentSidebar', () => { ...@@ -111,7 +111,7 @@ describe('BoardContentSidebar', () => {
}); });
it('renders BoardSidebarSubscription', () => { it('renders BoardSidebarSubscription', () => {
expect(wrapper.find(BoardSidebarSubscription).exists()).toBe(true); expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
}); });
it('renders BoardSidebarMilestoneSelect', () => { it('renders BoardSidebarMilestoneSelect', () => {
......
import { GlIcon, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
import { issueSubscriptionsResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Subscriptions Widget', () => {
let wrapper;
let fakeApollo;
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findToggle = () => wrapper.findComponent(GlToggle);
const findIcon = () => wrapper.findComponent(GlIcon);
const createComponent = ({
subscriptionsQueryHandler = jest.fn().mockResolvedValue(issueSubscriptionsResponse()),
} = {}) => {
fakeApollo = createMockApollo([[issueSubscribedQuery, subscriptionsQueryHandler]]);
wrapper = shallowMount(SidebarSubscriptionWidget, {
apolloProvider: fakeApollo,
provide: {
canUpdate: true,
},
propsData: {
fullPath: 'group/project',
iid: '1',
issuableType: 'issue',
},
stubs: {
SidebarEditableItem,
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to editable item when query is loading', () => {
createComponent();
expect(findEditableItem().props('loading')).toBe(true);
});
describe('when user is not subscribed to the issue', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('toggle is unchecked', () => {
expect(findToggle().props('value')).toBe(false);
});
it('emits `subscribedUpdated` event with a `false` payload', () => {
expect(wrapper.emitted('subscribedUpdated')).toEqual([[false]]);
});
});
describe('when user is subscribed to the issue', () => {
beforeEach(() => {
createComponent({
subscriptionsQueryHandler: jest.fn().mockResolvedValue(issueSubscriptionsResponse(true)),
});
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('toggle is checked', () => {
expect(findToggle().props('value')).toBe(true);
});
it('emits `subscribedUpdated` event with a `true` payload', () => {
expect(wrapper.emitted('subscribedUpdated')).toEqual([[true]]);
});
});
describe('when emails are disabled', () => {
it('toggle is disabled and off when user is subscribed', async () => {
createComponent({
subscriptionsQueryHandler: jest
.fn()
.mockResolvedValue(issueSubscriptionsResponse(true, true)),
});
await waitForPromises();
expect(findIcon().props('name')).toBe('notifications-off');
expect(findToggle().props('disabled')).toBe(true);
});
it('toggle is disabled and off when user is not subscribed', async () => {
createComponent({
subscriptionsQueryHandler: jest
.fn()
.mockResolvedValue(issueSubscriptionsResponse(false, true)),
});
await waitForPromises();
expect(findIcon().props('name')).toBe('notifications-off');
expect(findToggle().props('disabled')).toBe(true);
});
});
it('displays a flash message when query is rejected', async () => {
createComponent({
subscriptionsQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
});
...@@ -275,6 +275,20 @@ export const issueReferenceResponse = (reference) => ({ ...@@ -275,6 +275,20 @@ export const issueReferenceResponse = (reference) => ({
}, },
}); });
export const issueSubscriptionsResponse = (subscribed = false, emailsDisabled = false) => ({
data: {
workspace: {
__typename: 'Project',
issuable: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/4',
subscribed,
emailsDisabled,
},
},
},
});
export const issuableQueryResponse = { export const issuableQueryResponse = {
data: { data: {
workspace: { workspace: {
......
import { shallowMount } from '@vue/test-utils';
import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
describe('Sidebar Subscriptions', () => {
let wrapper;
let mediator;
beforeEach(() => {
mediator = new SidebarMediator(Mock.mediator);
wrapper = shallowMount(SidebarSubscriptions, {
propsData: {
mediator,
},
});
});
afterEach(() => {
wrapper.destroy();
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
});
it('calls the mediator toggleSubscription on event', () => {
const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve());
wrapper.vm.onToggleSubscription();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
...@@ -44,22 +44,24 @@ RSpec.shared_examples 'issue boards sidebar' do ...@@ -44,22 +44,24 @@ RSpec.shared_examples 'issue boards sidebar' do
context 'in notifications subscription' do context 'in notifications subscription' do
it 'displays notifications toggle', :aggregate_failures do it 'displays notifications toggle', :aggregate_failures do
page.within('[data-testid="sidebar-notifications"]') do page.within('[data-testid="sidebar-notifications"]') do
expect(page).to have_selector('[data-testid="notification-subscribe-toggle"]') expect(page).to have_selector('[data-testid="subscription-toggle"]')
expect(page).to have_content('Notifications') expect(page).to have_content('Notifications')
expect(page).not_to have_content('Notifications have been disabled by the project or group owner') expect(page).not_to have_content('Disabled by project owner')
end end
end end
it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do
toggle = find('[data-testid="notification-subscribe-toggle"]') wait_for_requests
toggle.click click_button 'Notifications'
expect(toggle).to have_css("button.is-checked") expect(page).to have_button('Notifications', class: 'is-checked')
toggle.click click_button 'Notifications'
expect(toggle).not_to have_css("button.is-checked") wait_for_requests
expect(page).not_to have_button('Notifications', class: 'is-checked')
end end
context 'when notifications have been disabled' do context 'when notifications have been disabled' do
...@@ -71,8 +73,8 @@ RSpec.shared_examples 'issue boards sidebar' do ...@@ -71,8 +73,8 @@ RSpec.shared_examples 'issue boards sidebar' do
it 'displays a message that notifications have been disabled' do it 'displays a message that notifications have been disabled' do
page.within('[data-testid="sidebar-notifications"]') do page.within('[data-testid="sidebar-notifications"]') do
expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]') expect(page).to have_button('Notifications', class: 'is-disabled')
expect(page).to have_content('Notifications have been disabled by the project or group owner') expect(page).to have_content('Disabled by project owner')
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment