Commit 15fea8a4 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '231401-epic-board-sidebar-edit-notifications' into 'master'

Epic board sidebar - Edit subscribed state [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!59214
parents 06ab148d f1f2b5b3
...@@ -21,26 +21,30 @@ export default { ...@@ -21,26 +21,30 @@ export default {
components: { components: {
GlToggle, GlToggle,
}, },
inject: ['emailsDisabled'],
data() { data() {
return { return {
loading: false, loading: false,
}; };
}, },
computed: { computed: {
...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']), ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue', 'isEpicBoard']),
isEmailsDisabled() {
return this.isEpicBoard ? this.emailsDisabled : this.activeBoardItem.emailsDisabled;
},
notificationText() { notificationText() {
return this.activeBoardItem.emailsDisabled return this.isEmailsDisabled
? this.$options.i18n.header.subscribeDisabledDescription ? this.$options.i18n.header.subscribeDisabledDescription
: this.$options.i18n.header.title; : this.$options.i18n.header.title;
}, },
}, },
methods: { methods: {
...mapActions(['setActiveIssueSubscribed']), ...mapActions(['setActiveItemSubscribed']),
async handleToggleSubscription() { async handleToggleSubscription() {
this.loading = true; this.loading = true;
try { try {
await this.setActiveIssueSubscribed({ await this.setActiveItemSubscribed({
subscribed: !this.activeBoardItem.subscribed, subscribed: !this.activeBoardItem.subscribed,
projectPath: this.projectPathForActiveIssue, projectPath: this.projectPathForActiveIssue,
}); });
...@@ -61,7 +65,7 @@ export default { ...@@ -61,7 +65,7 @@ export default {
> >
<span data-testid="notification-header-text"> {{ notificationText }} </span> <span data-testid="notification-header-text"> {{ notificationText }} </span>
<gl-toggle <gl-toggle
v-if="!activeBoardItem.emailsDisabled" v-if="!isEmailsDisabled"
:value="activeBoardItem.subscribed" :value="activeBoardItem.subscribed"
:is-loading="loading" :is-loading="loading"
:label="$options.i18n.header.title" :label="$options.i18n.header.title"
......
import { __ } from '~/locale'; import { __ } from '~/locale';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql'; import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql'; import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql'; import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
export const issuableTypes = { export const issuableTypes = {
...@@ -63,3 +65,12 @@ export const titleQueries = { ...@@ -63,3 +65,12 @@ export const titleQueries = {
mutation: updateEpicTitleMutation, mutation: updateEpicTitleMutation,
}, },
}; };
export const subscriptionQueries = {
[issuableTypes.issue]: {
mutation: issueSetSubscriptionMutation,
},
[issuableTypes.epic]: {
mutation: updateEpicSubscriptionMutation,
},
};
mutation issueSetSubscription($input: IssueSetSubscriptionInput!) { mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
issueSetSubscription(input: $input) { updateIssuableSubscription: issueSetSubscription(input: $input) {
issue { issue {
subscribed subscribed
} }
......
...@@ -95,6 +95,7 @@ export default () => { ...@@ -95,6 +95,7 @@ export default () => {
assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable), assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable), iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue, issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled),
}, },
store, store,
apolloProvider, apolloProvider,
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
flashAnimationDuration, flashAnimationDuration,
ISSUABLE, ISSUABLE,
titleQueries, titleQueries,
subscriptionQueries,
} from '~/boards/constants'; } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
...@@ -35,7 +36,6 @@ import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; ...@@ -35,7 +36,6 @@ import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql'; import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -597,26 +597,31 @@ export default { ...@@ -597,26 +597,31 @@ export default {
}); });
}, },
setActiveIssueSubscribed: async ({ commit, getters }, input) => { setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
const { activeBoardItem, isEpicBoard } = getters;
const { fullPath, issuableType } = state;
const workspacePath = isEpicBoard
? { groupPath: fullPath }
: { projectPath: input.projectPath };
const { data } = await gqlClient.mutate({ const { data } = await gqlClient.mutate({
mutation: issueSetSubscriptionMutation, mutation: subscriptionQueries[issuableType].mutation,
variables: { variables: {
input: { input: {
iid: String(getters.activeBoardItem.iid), ...workspacePath,
projectPath: input.projectPath, iid: String(activeBoardItem.iid),
subscribedState: input.subscribed, subscribedState: input.subscribed,
}, },
}, },
}); });
if (data.issueSetSubscription?.errors?.length > 0) { if (data.updateIssuableSubscription?.errors?.length > 0) {
throw new Error(data.issueSetSubscription.errors); throw new Error(data.updateIssuableSubscription[issuableType].errors);
} }
commit(types.UPDATE_BOARD_ITEM_BY_ID, { commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: getters.activeBoardItem.id, itemId: activeBoardItem.id,
prop: 'subscribed', prop: 'subscribed',
value: data.issueSetSubscription.issue.subscribed, value: data.updateIssuableSubscription[issuableType].subscribed,
}); });
}, },
......
mutation epicSetSubscription($input: EpicSetSubscriptionInput!) {
updateIssuableSubscription: epicSetSubscription(input: $input) {
epic {
subscribed
}
errors
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
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';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
components: { components: {
GlDrawer, GlDrawer,
BoardSidebarLabelsSelect, BoardSidebarLabelsSelect,
BoardSidebarSubscription,
BoardSidebarTitle, BoardSidebarTitle,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
}, },
...@@ -51,6 +53,7 @@ export default { ...@@ -51,6 +53,7 @@ export default {
issuable-type="epic" issuable-type="epic"
@confidentialityUpdated="setActiveEpicConfidential($event)" @confidentialityUpdated="setActiveEpicConfidential($event)"
/> />
<board-sidebar-subscription class="subscriptions" />
</template> </template>
</gl-drawer> </gl-drawer>
</template> </template>
...@@ -21,6 +21,7 @@ query ListEpics( ...@@ -21,6 +21,7 @@ query ListEpics(
relativePosition relativePosition
referencePath: reference(full: true) referencePath: reference(full: true)
confidential confidential
subscribed
labels { labels {
nodes { nodes {
...Label ...Label
......
...@@ -76,6 +76,7 @@ export default () => { ...@@ -76,6 +76,7 @@ export default () => {
milestoneListsAvailable: false, milestoneListsAvailable: false,
assigneeListsAvailable: false, assigneeListsAvailable: false,
iterationListsAvailable: false, iterationListsAvailable: false,
emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled),
}, },
store, store,
apolloProvider, apolloProvider,
......
...@@ -32,7 +32,8 @@ module EE ...@@ -32,7 +32,8 @@ module EE
show_promotion: show_feature_promotion, show_promotion: show_feature_promotion,
scoped_labels: current_board_parent.feature_available?(:scoped_labels)&.to_s, scoped_labels: current_board_parent.feature_available?(:scoped_labels)&.to_s,
can_update: can_update?.to_s, can_update: can_update?.to_s,
can_admin_list: can_admin_list?.to_s can_admin_list: can_admin_list?.to_s,
emails_disabled: current_board_parent.emails_disabled?.to_s
} }
super.merge(data) super.merge(data)
......
...@@ -45,7 +45,7 @@ RSpec.describe 'Epic boards sidebar', :js do ...@@ -45,7 +45,7 @@ RSpec.describe 'Epic boards sidebar', :js do
expect(page).to have_selector('[data-testid="epic-boards-sidebar"]') expect(page).to have_selector('[data-testid="epic-boards-sidebar"]')
find('[data-testid="close-icon"]').click find('.gl-drawer-close-button [data-testid="close-icon"]').click
expect(page).not_to have_selector('[data-testid="epic-boards-sidebar"]') expect(page).not_to have_selector('[data-testid="epic-boards-sidebar"]')
end end
...@@ -114,11 +114,11 @@ RSpec.describe 'Epic boards sidebar', :js do ...@@ -114,11 +114,11 @@ RSpec.describe 'Epic boards sidebar', :js do
page.within('.confidentiality') do page.within('.confidentiality') do
expect(page).to have_content('Not confidential') expect(page).to have_content('Not confidential')
find('[data-testid="edit-button"]').click click_button 'Edit'
expect(page).to have_css('.sidebar-item-warning-message') expect(page).to have_css('.sidebar-item-warning-message')
within('.sidebar-item-warning-message') do within('.sidebar-item-warning-message') do
find('[data-testid="confidential-toggle"]').click click_button 'Turn on'
end end
wait_for_requests wait_for_requests
...@@ -127,4 +127,50 @@ RSpec.describe 'Epic boards sidebar', :js do ...@@ -127,4 +127,50 @@ RSpec.describe 'Epic boards sidebar', :js do
end end
end end
end end
context 'in notifications subscription' do
it 'displays notifications toggle', :aggregate_failures do
click_card(card)
page.within('[data-testid="sidebar-notifications"]') do
expect(page).to have_button('Notifications')
expect(page).not_to have_content('Notifications have been disabled by the project or group owner')
end
end
it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do
click_card(card)
click_button 'Notifications'
expect(page).to have_button('Notifications', class: 'is-checked')
click_button 'Notifications'
expect(page).not_to have_button('Notifications', class: 'is-checked')
end
context 'when notifications have been disabled' do
before do
group.update_attribute(:emails_disabled, true)
refresh_and_click_first_card
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')
end
end
end
end
def refresh_and_click_first_card
page.refresh
wait_for_requests
click_card(card)
end
end end
import { GlToggle, GlLoadingIcon } from '@gitlab/ui'; import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import { createStore } from '~/boards/stores'; import { createStore } from '~/boards/stores';
...@@ -9,8 +10,7 @@ import { mockActiveIssue } from '../../mock_data'; ...@@ -9,8 +10,7 @@ import { mockActiveIssue } from '../../mock_data';
jest.mock('~/flash.js'); jest.mock('~/flash.js');
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => { describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
let wrapper; let wrapper;
...@@ -26,8 +26,10 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = ...@@ -26,8 +26,10 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
store.state.activeId = activeBoardItem.id; store.state.activeId = activeBoardItem.id;
wrapper = mount(BoardSidebarSubscription, { wrapper = mount(BoardSidebarSubscription, {
localVue,
store, store,
provide: {
emailsDisabled: false,
},
}); });
}; };
...@@ -90,7 +92,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = ...@@ -90,7 +92,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
describe('Board sidebar subscription component `behavior`', () => { describe('Board sidebar subscription component `behavior`', () => {
const mockSetActiveIssueSubscribed = (subscribedState) => { const mockSetActiveIssueSubscribed = (subscribedState) => {
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
store.commit(types.UPDATE_BOARD_ITEM_BY_ID, { store.commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: mockActiveIssue.id, itemId: mockActiveIssue.id,
prop: 'subscribed', prop: 'subscribed',
...@@ -110,7 +112,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = ...@@ -110,7 +112,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true); expect(findGlLoadingIcon().exists()).toBe(true);
expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({
subscribed: true, subscribed: true,
projectPath: 'gitlab-org/test-subgroup/gitlab-test', projectPath: 'gitlab-org/test-subgroup/gitlab-test',
}); });
...@@ -134,7 +136,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = ...@@ -134,7 +136,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({
subscribed: false, subscribed: false,
projectPath: 'gitlab-org/test-subgroup/gitlab-test', projectPath: 'gitlab-org/test-subgroup/gitlab-test',
}); });
...@@ -148,7 +150,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = ...@@ -148,7 +150,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
it('flashes an error message when setting the subscribed state fails', async () => { it('flashes an error message when setting the subscribed state fails', async () => {
createComponent(); createComponent();
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
throw new Error(); throw new Error();
}); });
......
...@@ -1356,9 +1356,15 @@ describe('setActiveIssueDueDate', () => { ...@@ -1356,9 +1356,15 @@ describe('setActiveIssueDueDate', () => {
}); });
}); });
describe('setActiveIssueSubscribed', () => { describe('setActiveItemSubscribed', () => {
const state = { boardItems: { [mockActiveIssue.id]: mockActiveIssue } }; const state = {
const getters = { activeBoardItem: mockActiveIssue }; boardItems: {
[mockActiveIssue.id]: mockActiveIssue,
},
fullPath: 'gitlab-org',
issuableType: 'issue',
};
const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false };
const subscribedState = true; const subscribedState = true;
const input = { const input = {
subscribedState, subscribedState,
...@@ -1368,7 +1374,7 @@ describe('setActiveIssueSubscribed', () => { ...@@ -1368,7 +1374,7 @@ describe('setActiveIssueSubscribed', () => {
it('should commit subscribed status', (done) => { it('should commit subscribed status', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: { data: {
issueSetSubscription: { updateIssuableSubscription: {
issue: { issue: {
subscribed: subscribedState, subscribed: subscribedState,
}, },
...@@ -1384,7 +1390,7 @@ describe('setActiveIssueSubscribed', () => { ...@@ -1384,7 +1390,7 @@ describe('setActiveIssueSubscribed', () => {
}; };
testAction( testAction(
actions.setActiveIssueSubscribed, actions.setActiveItemSubscribed,
input, input,
{ ...state, ...getters }, { ...state, ...getters },
[ [
...@@ -1401,9 +1407,9 @@ describe('setActiveIssueSubscribed', () => { ...@@ -1401,9 +1407,9 @@ describe('setActiveIssueSubscribed', () => {
it('throws error if fails', async () => { it('throws error if fails', async () => {
jest jest
.spyOn(gqlClient, 'mutate') .spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } }); .mockResolvedValue({ data: { updateIssuableSubscription: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error); await expect(actions.setActiveItemSubscribed({ getters }, input)).rejects.toThrow(Error);
}); });
}); });
......
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