Commit 529831ec authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Simon Knox

Add notifications toggle to swimlanes sidebar

parent a5032437
...@@ -18,7 +18,7 @@ export default { ...@@ -18,7 +18,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({ issue: 'activeIssue' }), ...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }),
hasDueDate() { hasDueDate() {
return this.issue.dueDate != null; return this.issue.dueDate != null;
}, },
...@@ -36,10 +36,6 @@ export default { ...@@ -36,10 +36,6 @@ export default {
return dateInWords(this.parsedDueDate, true); return dateInWords(this.parsedDueDate, true);
}, },
projectPath() {
const referencePath = this.issue.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
}, },
methods: { methods: {
...mapActions(['setActiveIssueDueDate']), ...mapActions(['setActiveIssueDueDate']),
...@@ -53,7 +49,7 @@ export default { ...@@ -53,7 +49,7 @@ export default {
try { try {
const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null; const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPath }); await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
} catch (e) { } catch (e) {
createFlash({ message: this.$options.i18n.updateDueDateError }); createFlash({ message: this.$options.i18n.updateDueDateError });
} finally { } finally {
......
...@@ -21,9 +21,9 @@ export default { ...@@ -21,9 +21,9 @@ export default {
}, },
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: { computed: {
...mapGetters({ issue: 'activeIssue' }), ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
selectedLabels() { selectedLabels() {
const { labels = [] } = this.issue; const { labels = [] } = this.activeIssue;
return labels.map(label => ({ return labels.map(label => ({
...label, ...label,
...@@ -31,17 +31,13 @@ export default { ...@@ -31,17 +31,13 @@ export default {
})); }));
}, },
issueLabels() { issueLabels() {
const { labels = [] } = this.issue; const { labels = [] } = this.activeIssue;
return labels.map(label => ({ return labels.map(label => ({
...label, ...label,
scoped: isScopedLabel(label), scoped: isScopedLabel(label),
})); }));
}, },
projectPath() {
const { referencePath = '' } = this.issue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
}, },
methods: { methods: {
...mapActions(['setActiveIssueLabels']), ...mapActions(['setActiveIssueLabels']),
...@@ -55,7 +51,7 @@ export default { ...@@ -55,7 +51,7 @@ export default {
.filter(label => !payload.find(selected => selected.id === label.id)) .filter(label => !payload.find(selected => selected.id === label.id))
.map(label => label.id); .map(label => label.id);
const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath }; const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input); await this.setActiveIssueLabels(input);
} catch (e) { } catch (e) {
createFlash({ message: __('An error occurred while updating labels.') }); createFlash({ message: __('An error occurred while updating labels.') });
...@@ -68,7 +64,7 @@ export default { ...@@ -68,7 +64,7 @@ export default {
try { try {
const removeLabelIds = [getIdFromGraphQLId(id)]; const removeLabelIds = [getIdFromGraphQLId(id)];
const input = { removeLabelIds, projectPath: this.projectPath }; const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input); await this.setActiveIssueLabels(input);
} catch (e) { } catch (e) {
createFlash({ message: __('An error occurred when removing the label.') }); createFlash({ message: __('An error occurred when removing the label.') });
......
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlToggle } from '@gitlab/ui';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
export default {
i18n: {
header: {
title: __('Notifications'),
/* Any change to subscribeDisabledDescription
must be reflected in app/helpers/notifications_helper.rb */
subscribeDisabledDescription: __(
'Notifications have been disabled by the project or group owner',
),
},
updateSubscribedErrorMessage: s__(
'IssueBoards|An error occurred while setting notifications status.',
),
},
components: {
GlToggle,
},
data() {
return {
loading: false,
};
},
computed: {
...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
notificationText() {
return this.activeIssue.emailsDisabled
? this.$options.i18n.header.subscribeDisabledDescription
: this.$options.i18n.header.title;
},
},
methods: {
...mapActions(['setActiveIssueSubscribed']),
async handleToggleSubscription() {
this.loading = true;
try {
await this.setActiveIssueSubscribed({
subscribed: !this.activeIssue.subscribed,
projectPath: this.projectPathForActiveIssue,
});
} catch (error) {
createFlash({ message: this.$options.i18n.updateSubscribedErrorMessage });
} finally {
this.loading = false;
}
},
},
};
</script>
<template>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between"
data-testid="sidebar-notifications"
>
<span data-testid="notification-header-text"> {{ notificationText }} </span>
<gl-toggle
v-if="!activeIssue.emailsDisabled"
:value="activeIssue.subscribed"
:is-loading="loading"
data-testid="notification-subscribe-toggle"
@change="handleToggleSubscription"
/>
</div>
</template>
mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
issueSetSubscription(input: $input) {
issue {
subscribed
}
errors
}
}
...@@ -24,6 +24,7 @@ import destroyBoardListMutation from '../queries/board_list_destroy.mutation.gra ...@@ -24,6 +24,7 @@ import destroyBoardListMutation from '../queries/board_list_destroy.mutation.gra
import issueCreateMutation from '../queries/issue_create.mutation.graphql'; import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql'; import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql'; import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
const notImplemented = () => { const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */ /* eslint-disable-next-line @gitlab/require-i18n-strings */
...@@ -420,6 +421,29 @@ export default { ...@@ -420,6 +421,29 @@ export default {
}); });
}, },
setActiveIssueSubscribed: async ({ commit, getters }, input) => {
const { data } = await gqlClient.mutate({
mutation: issueSetSubscriptionMutation,
variables: {
input: {
iid: String(getters.activeIssue.iid),
projectPath: input.projectPath,
subscribedState: input.subscribed,
},
},
});
if (data.issueSetSubscription?.errors?.length > 0) {
throw new Error(data.issueSetSubscription.errors);
}
commit(types.UPDATE_ISSUE_BY_ID, {
issueId: getters.activeIssue.id,
prop: 'subscribed',
value: data.issueSetSubscription.issue.subscribed,
});
},
fetchBacklog: () => { fetchBacklog: () => {
notImplemented(); notImplemented();
}, },
......
...@@ -24,6 +24,11 @@ export default { ...@@ -24,6 +24,11 @@ export default {
return state.issues[state.activeId] || {}; return state.issues[state.activeId] || {};
}, },
projectPathForActiveIssue: (_, getters) => {
const referencePath = getters.activeIssue.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
getListByLabelId: state => labelId => { getListByLabelId: state => labelId => {
return find(state.boardLists, l => l.label?.id === labelId); return find(state.boardLists, l => l.label?.id === labelId);
}, },
......
...@@ -67,6 +67,7 @@ module NotificationsHelper ...@@ -67,6 +67,7 @@ module NotificationsHelper
when :custom when :custom
_('You will only receive notifications for the events you choose') _('You will only receive notifications for the events you choose')
when :owner_disabled when :owner_disabled
# Any change must be reflected in board_sidebar_subscription.vue
_('Notifications have been disabled by the project or group owner') _('Notifications have been disabled by the project or group owner')
end end
end end
......
...@@ -11,6 +11,7 @@ import BoardSidebarTimeTracker from './sidebar/board_sidebar_time_tracker.vue'; ...@@ -11,6 +11,7 @@ import BoardSidebarTimeTracker from './sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarWeightInput from './sidebar/board_sidebar_weight_input.vue'; import BoardSidebarWeightInput from './sidebar/board_sidebar_weight_input.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
export default { export default {
headerHeight: `${contentTop()}px`, headerHeight: `${contentTop()}px`,
...@@ -23,6 +24,7 @@ export default { ...@@ -23,6 +24,7 @@ export default {
BoardSidebarWeightInput, BoardSidebarWeightInput,
BoardSidebarLabelsSelect, BoardSidebarLabelsSelect,
BoardSidebarDueDate, BoardSidebarDueDate,
BoardSidebarSubscription,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...@@ -56,6 +58,7 @@ export default { ...@@ -56,6 +58,7 @@ export default {
<board-sidebar-weight-input v-if="glFeatures.issueWeights" /> <board-sidebar-weight-input v-if="glFeatures.issueWeights" />
<board-sidebar-labels-select /> <board-sidebar-labels-select />
<board-sidebar-due-date /> <board-sidebar-due-date />
<board-sidebar-subscription />
</template> </template>
</gl-drawer> </gl-drawer>
</template> </template>
...@@ -20,9 +20,9 @@ export default { ...@@ -20,9 +20,9 @@ export default {
inject: ['groupId'], inject: ['groupId'],
computed: { computed: {
...mapState(['epics']), ...mapState(['epics']),
...mapGetters({ getEpicById: 'getEpicById', issue: 'activeIssue' }), ...mapGetters(['activeIssue', 'getEpicById', 'projectPathForActiveIssue']),
storedEpic() { storedEpic() {
const storedEpic = this.getEpicById(this.issue.epic?.id); const storedEpic = this.getEpicById(this.activeIssue.epic?.id);
const epicId = getIdFromGraphQLId(storedEpic?.id); const epicId = getIdFromGraphQLId(storedEpic?.id);
return { return {
...@@ -30,10 +30,6 @@ export default { ...@@ -30,10 +30,6 @@ export default {
id: Number(epicId), id: Number(epicId),
}; };
}, },
projectPath() {
const { referencePath = '' } = this.issue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
...@@ -51,7 +47,7 @@ export default { ...@@ -51,7 +47,7 @@ export default {
const epicId = selectedEpic?.id ? `gid://gitlab/Epic/${selectedEpic.id}` : null; const epicId = selectedEpic?.id ? `gid://gitlab/Epic/${selectedEpic.id}` : null;
const input = { const input = {
epicId, epicId,
projectPath: this.projectPath, projectPath: this.projectPathForActiveIssue,
}; };
try { try {
...@@ -62,7 +58,7 @@ export default { ...@@ -62,7 +58,7 @@ export default {
} }
debounceByAnimationFrame(() => { debounceByAnimationFrame(() => {
this.updateIssueById({ issueId: this.issue.id, prop: 'epic', value: epic }); this.updateIssueById({ issueId: this.activeIssue.id, prop: 'epic', value: epic });
this.loading = false; this.loading = false;
})(); })();
} catch (e) { } catch (e) {
......
...@@ -23,14 +23,10 @@ export default { ...@@ -23,14 +23,10 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({ issue: 'activeIssue' }), ...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }),
hasWeight() { hasWeight() {
return this.issue.weight > 0; return this.issue.weight > 0;
}, },
projectPath() {
const { referencePath = '' } = this.issue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
}, },
watch: { watch: {
issue: { issue: {
...@@ -56,7 +52,7 @@ export default { ...@@ -56,7 +52,7 @@ export default {
this.loading = true; this.loading = true;
try { try {
await this.setActiveIssueWeight({ weight, projectPath: this.projectPath }); await this.setActiveIssueWeight({ weight, projectPath: this.projectPathForActiveIssue });
this.weight = weight; this.weight = weight;
} catch (e) { } catch (e) {
this.weight = this.issue.weight; this.weight = this.issue.weight;
......
...@@ -10,6 +10,7 @@ fragment IssueNode on Issue { ...@@ -10,6 +10,7 @@ fragment IssueNode on Issue {
totalTimeSpent totalTimeSpent
humanTimeEstimate humanTimeEstimate
humanTotalTimeSpent humanTotalTimeSpent
emailsDisabled
weight weight
confidential confidential
webUrl webUrl
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'epics swimlanes sidebar', :js do RSpec.describe 'epics swimlanes sidebar', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:project, reload: true) { create(:project, :public, group: group) }
let_it_be(:board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
let_it_be(:list) { create(:list, board: board, position: 0) } let_it_be(:list) { create(:list, board: board, position: 0) }
...@@ -31,6 +31,47 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -31,6 +31,47 @@ RSpec.describe 'epics swimlanes sidebar', :js do
wait_for_all_requests wait_for_all_requests
end end
context 'notifications subscription' do
it 'displays notifications toggle' do
click_first_issue_card
page.within('[data-testid="sidebar-notifications"]') do
expect(page).to have_selector('[data-testid="notification-subscribe-toggle"]')
expect(page).to have_content('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' do
click_first_issue_card
toggle = find('[data-testid="notification-subscribe-toggle"]')
toggle.click
expect(toggle).to have_css("button.is-checked")
toggle.click
expect(toggle).not_to have_css("button.is-checked")
end
context 'when notifications have been disabled' do
before do
project.update_attribute(:emails_disabled, true)
end
it 'displays a message that notifications have been disabled' do
click_first_issue_card
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
context 'time tracking' do context 'time tracking' do
it 'displays time tracking feature with default message' do it 'displays time tracking feature with default message' do
click_first_issue_card click_first_issue_card
......
...@@ -24,6 +24,7 @@ describe('ee/BoardContentSidebar', () => { ...@@ -24,6 +24,7 @@ describe('ee/BoardContentSidebar', () => {
'board-sidebar-weight-input': '<div></div>', 'board-sidebar-weight-input': '<div></div>',
'board-sidebar-labels-select': '<div></div>', 'board-sidebar-labels-select': '<div></div>',
'board-sidebar-due-date': '<div></div>', 'board-sidebar-due-date': '<div></div>',
'board-sidebar-subscription': '<div></div>',
}, },
}); });
}; };
......
...@@ -15089,6 +15089,9 @@ msgstr "" ...@@ -15089,6 +15089,9 @@ msgstr ""
msgid "IssueAnalytics|Weight" msgid "IssueAnalytics|Weight"
msgstr "" msgstr ""
msgid "IssueBoards|An error occurred while setting notifications status."
msgstr ""
msgid "IssueBoards|Board" msgid "IssueBoards|Board"
msgstr "" msgstr ""
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import * as types from '~/boards/stores/mutation_types';
import { createStore } from '~/boards/stores';
import { mockActiveIssue } from '../../mock_data';
import createFlash from '~/flash';
jest.mock('~/flash.js');
const localVue = createLocalVue();
localVue.use(Vuex);
describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
let wrapper;
let store;
const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
const findToggle = () => wrapper.find(GlToggle);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const createComponent = (activeIssue = { ...mockActiveIssue }) => {
store = createStore();
store.state.issues = { [activeIssue.id]: activeIssue };
store.state.activeId = activeIssue.id;
wrapper = mount(BoardSidebarSubscription, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
store = null;
jest.clearAllMocks();
});
describe('Board sidebar subscription component template', () => {
it('displays "notifications" heading', () => {
createComponent();
expect(findNotificationHeader().text()).toBe('Notifications');
});
it('renders toggle as "off" when currently not subscribed', () => {
createComponent();
expect(findToggle().exists()).toBe(true);
expect(findToggle().props('value')).toBe(false);
});
it('renders toggle as "on" when currently subscribed', () => {
createComponent({
...mockActiveIssue,
subscribed: true,
});
expect(findToggle().exists()).toBe(true);
expect(findToggle().props('value')).toBe(true);
});
describe('when notification emails have been disabled', () => {
beforeEach(() => {
createComponent({
...mockActiveIssue,
emailsDisabled: true,
});
});
it('displays a message that notification have been disabled', () => {
expect(findNotificationHeader().text()).toBe(
'Notifications have been disabled by the project or group owner',
);
});
it('does not render the toggle button', () => {
expect(findToggle().exists()).toBe(false);
});
});
});
describe('Board sidebar subscription component `behavior`', () => {
const mockSetActiveIssueSubscribed = subscribedState => {
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
store.commit(types.UPDATE_ISSUE_BY_ID, {
issueId: mockActiveIssue.id,
prop: 'subscribed',
value: subscribedState,
});
});
};
it('subscribing to notification', async () => {
createComponent();
mockSetActiveIssueSubscribed(true);
expect(findGlLoadingIcon().exists()).toBe(false);
findToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
subscribed: true,
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(false);
expect(findToggle().props('value')).toBe(true);
});
it('unsubscribing from notification', async () => {
createComponent({
...mockActiveIssue,
subscribed: true,
});
mockSetActiveIssueSubscribed(false);
expect(findGlLoadingIcon().exists()).toBe(false);
findToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
subscribed: false,
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
expect(findGlLoadingIcon().exists()).toBe(true);
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(false);
expect(findToggle().props('value')).toBe(false);
});
it('flashes an error message when setting the subscribed state fails', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
throw new Error();
});
findToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenNthCalledWith(1, {
message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
});
});
});
});
...@@ -176,6 +176,14 @@ export const mockIssue = { ...@@ -176,6 +176,14 @@ export const mockIssue = {
}, },
}; };
export const mockActiveIssue = {
...mockIssue,
id: 436,
iid: '27',
subscribed: false,
emailsDisabled: false,
};
export const mockIssueWithModel = new ListIssue(mockIssue); export const mockIssueWithModel = new ListIssue(mockIssue);
export const mockIssue2 = { export const mockIssue2 = {
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
rawIssue, rawIssue,
mockIssues, mockIssues,
labels, labels,
mockActiveIssue,
} from '../mock_data'; } from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions'; import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
...@@ -833,6 +834,57 @@ describe('setActiveIssueDueDate', () => { ...@@ -833,6 +834,57 @@ describe('setActiveIssueDueDate', () => {
}); });
}); });
describe('setActiveIssueSubscribed', () => {
const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } };
const getters = { activeIssue: mockActiveIssue };
const subscribedState = true;
const input = {
subscribedState,
projectPath: 'gitlab-org/gitlab-test',
};
it('should commit subscribed status', done => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueSetSubscription: {
issue: {
subscribed: subscribedState,
},
errors: [],
},
},
});
const payload = {
issueId: getters.activeIssue.id,
prop: 'subscribed',
value: subscribedState,
};
testAction(
actions.setActiveIssueSubscribed,
input,
{ ...state, ...getters },
[
{
type: types.UPDATE_ISSUE_BY_ID,
payload,
},
],
[],
done,
);
});
it('throws error if fails', async () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error);
});
});
describe('fetchBacklog', () => { describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog); expectNotImplemented(actions.fetchBacklog);
}); });
......
...@@ -124,6 +124,22 @@ describe('Boards - Getters', () => { ...@@ -124,6 +124,22 @@ describe('Boards - Getters', () => {
}); });
}); });
describe('projectPathByIssueId', () => {
it('returns project path for the active issue', () => {
const mockActiveIssue = {
referencePath: 'gitlab-org/gitlab-test#1',
};
expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
'gitlab-org/gitlab-test',
);
});
it('returns empty string as project when active issue is an empty object', () => {
const mockActiveIssue = {};
expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
});
});
describe('getIssuesByList', () => { describe('getIssuesByList', () => {
const boardsState = { const boardsState = {
issuesByListId: mockIssuesByListId, issuesByListId: mockIssuesByListId,
......
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