Commit 4f584519 authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

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

parent 2c4abf28
......@@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_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 BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_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';
export default {
......@@ -23,7 +23,7 @@ export default {
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
BoardSidebarDueDate,
BoardSidebarSubscription,
SidebarSubscriptionsWidget,
BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect: () =>
import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
......@@ -98,7 +98,12 @@ export default {
:issuable-type="issuableType"
@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>
</gl-drawer>
</template>
......@@ -43,6 +43,11 @@ export default {
property: null,
}),
},
canEdit: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -113,8 +118,9 @@ export default {
inline
class="gl-mx-auto gl-my-0 hide-expanded"
/>
<slot name="collapsed-right"></slot>
<gl-button
v-if="canUpdate && !initialLoading"
v-if="canUpdate && !initialLoading && canEdit"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
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';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_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 issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.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 mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.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 updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.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 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';
......@@ -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 = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
......
......@@ -24,7 +24,7 @@ import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.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 SidebarMoveIssue from './lib/sidebar_move_issue';
......@@ -334,21 +334,32 @@ function mountParticipantsComponent(mediator) {
});
}
function mountSubscriptionsComponent(mediator) {
function mountSubscriptionsComponent() {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
const { fullPath, iid, editable } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
components: {
sidebarSubscriptions,
SidebarSubscriptionsWidget,
},
provide: {
canUpdate: editable,
},
render: (createElement) =>
createElement('sidebar-subscriptions', {
createElement('sidebar-subscriptions-widget', {
props: {
mediator,
iid: String(iid),
fullPath,
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
},
}),
});
......@@ -425,7 +436,7 @@ export function mountSidebar(mediator) {
mountReferenceComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator);
mountSubscriptionsComponent();
mountCopyEmailComponent();
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!) {
updateIssuableSubscription: epicSetSubscription(input: $input) {
epic {
mutation epicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
updateIssuableSubscription: epicSetSubscription(
input: { groupPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
issuable: epic {
id
subscribed
}
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 @@
import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
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 { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default {
headerHeight: `${contentTop()}px`,
components: {
GlDrawer,
BoardSidebarLabelsSelect,
BoardSidebarSubscription,
BoardSidebarTitle,
SidebarConfidentialityWidget,
SidebarDateWidget,
SidebarSubscriptionsWidget,
},
computed: {
...mapGetters(['isSidebarOpen', 'activeBoardItem']),
...mapState(['sidebarType', 'fullPath']),
...mapState(['sidebarType', 'fullPath', 'issuableType']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
......@@ -30,7 +30,7 @@ export default {
},
},
methods: {
...mapActions(['toggleBoardItem', 'setActiveItemConfidential']),
...mapActions(['toggleBoardItem', 'setActiveItemConfidential', 'setActiveItemSubscribed']),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
......@@ -52,24 +52,28 @@ export default {
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="startDate"
issuable-type="epic"
:issuable-type="issuableType"
:can-inherit="true"
/>
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="dueDate"
issuable-type="epic"
:issuable-type="issuableType"
:can-inherit="true"
/>
<board-sidebar-labels-select class="labels" />
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
issuable-type="epic"
:issuable-type="issuableType"
@confidentialityUpdated="setActiveItemConfidential($event)"
/>
<board-sidebar-subscription class="subscriptions" />
<sidebar-subscriptions-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
/>
</template>
</gl-drawer>
</template>
......@@ -3,9 +3,11 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
import { IssuableType } from '~/issue_show/constants';
import notesEventHub from '~/notes/event_hub';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.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 SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
......@@ -14,7 +16,6 @@ import epicUtils from '../utils/epic_utils';
import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue';
import SidebarHeader from './sidebar_items/sidebar_header.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';
export default {
......@@ -27,8 +28,8 @@ export default {
SidebarLabels,
AncestorsTree,
SidebarParticipants,
SidebarSubscription,
SidebarConfidentialityWidget,
SidebarSubscriptionsWidget,
},
inject: ['iid'],
data() {
......@@ -69,6 +70,9 @@ export default {
'dueDateForCollapsedSidebar',
'ancestors',
]),
issuableType() {
return IssuableType.Epic;
},
},
mounted() {
this.toggleSidebarFlag(epicUtils.getCollapsedGutter());
......@@ -225,7 +229,7 @@ export default {
<sidebar-confidentiality-widget
:iid="String(iid)"
:full-path="fullPath"
issuable-type="epic"
:issuable-type="issuableType"
@closeForm="handleSidebarToggle"
@expandSidebar="handleSidebarToggle"
@confidentialityUpdated="updateConfidentialityOnIssuable($event)"
......@@ -239,7 +243,13 @@ export default {
@toggleSidebar="toggleSidebar({ sidebarCollapsed })"
/>
</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>
</aside>
</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 () => {
groupId: parseInt($boardApp.dataset.groupId, 10),
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: $boardApp.dataset.canUpdate,
canUpdate: parseBoolean($boardApp.dataset.canUpdate),
canAdminList: parseBoolean($boardApp.dataset.canAdminList),
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
......
......@@ -176,21 +176,27 @@ RSpec.describe 'Epic boards sidebar', :js do
it 'displays notifications toggle', :aggregate_failures do
click_card(card)
page.within('[data-testid="sidebar-notifications"]') do
page.within('.subscriptions') do
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
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)
wait_for_requests
click_button 'Notifications'
wait_for_requests
expect(page).to have_button('Notifications', class: 'is-checked')
click_button 'Notifications'
wait_for_requests
expect(page).not_to have_button('Notifications', class: 'is-checked')
end
......@@ -202,9 +208,9 @@ RSpec.describe 'Epic boards sidebar', :js do
end
it 'displays a message that notifications have been disabled' do
page.within('[data-testid="sidebar-notifications"]') do
expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]')
expect(page).to have_content('Notifications have been disabled by the project or group owner')
page.within('.subscriptions') do
expect(page).to have_button('Notifications', class: 'is-disabled')
expect(page).to have_content('Disabled by group owner')
end
end
end
......
......@@ -6,6 +6,7 @@ import { stubComponent } from 'helpers/stub_component';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import { ISSUABLE } from '~/boards/constants';
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';
describe('EpicBoardContentSidebar', () => {
......@@ -80,6 +81,10 @@ describe('EpicBoardContentSidebar', () => {
expect(wrapper.find(SidebarConfidentialityWidget).exists()).toBe(true);
});
it('renders SidebarSubscriptionsWidget', () => {
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
});
describe('when we emit close', () => {
let toggleBoardItem;
......
......@@ -10,6 +10,8 @@ import epicUtils from 'ee/epic/utils/epic_utils';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockEpicMeta, mockEpicData, mockAncestors } from '../mock_data';
describe('EpicSidebarComponent', () => {
......@@ -207,6 +209,10 @@ describe('EpicSidebarComponent', () => {
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', () => {
it('renders ancestors list', async () => {
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 ""
msgid "Disabled"
msgstr ""
msgid "Disabled by %{parent} owner"
msgstr ""
msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
msgstr ""
......@@ -12830,9 +12833,6 @@ msgstr ""
msgid "Error occurred when saving reviewers"
msgstr ""
msgid "Error occurred when toggling the notification subscription"
msgstr ""
msgid "Error occurred while updating the issue status"
msgstr ""
......@@ -30044,6 +30044,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while setting %{issuableType} notifications."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
......
......@@ -32,8 +32,8 @@ RSpec.describe "User toggles subscription", :js do
let(:project) { create(:project_empty_repo, :public, emails_disabled: true) }
it 'is disabled' do
expect(page).to have_content('Notifications have been disabled by the project or group owner')
expect(page).not_to have_selector('[data-testid="subscription-toggle"]')
expect(page).to have_content('Disabled by project owner')
expect(page).to have_button('Notifications', class: 'is-disabled')
end
end
end
......@@ -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 BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_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 { ISSUABLE } from '~/boards/constants';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
describe('BoardContentSidebar', () => {
......@@ -111,7 +111,7 @@ describe('BoardContentSidebar', () => {
});
it('renders BoardSidebarSubscription', () => {
expect(wrapper.find(BoardSidebarSubscription).exists()).toBe(true);
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
});
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) => ({
},
});
export const issueSubscriptionsResponse = (subscribed = false, emailsDisabled = false) => ({
data: {
workspace: {
__typename: 'Project',
issuable: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/4',
subscribed,
emailsDisabled,
},
},
},
});
export const issuableQueryResponse = {
data: {
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
context 'in notifications subscription' do
it 'displays notifications toggle', :aggregate_failures 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).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
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
context 'when notifications have been disabled' do
......@@ -71,8 +73,8 @@ RSpec.shared_examples 'issue boards sidebar' do
it 'displays a message that notifications have been disabled' do
page.within('[data-testid="sidebar-notifications"]') do
expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]')
expect(page).to have_content('Notifications have been disabled by the project or group owner')
expect(page).to have_button('Notifications', class: 'is-disabled')
expect(page).to have_content('Disabled by project owner')
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