Commit e8832985 authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Kushal Pandya

Fix epic and milestone select in swimlane sidebar

For epic select in the swimlane sidebar:

1. The epic dropdown didn't automatically expand
because 'board_sidebar_epic_select.vue`
was using a removed method (handleEditClick)
of sidebar/epics_select/base.vue.

The method's been removed in
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52528.

This MR replaces the removed method with the newly added substitute
method 'toggleFormDropdown' in 'board_sidebar_epic_select.vue'

2. The epic dropdown threw an error
when user selects an already-assigned epic.

This MR fixes the problem by adding a guard.

For milestone select in the swimlane sidebar:

BoardSidebarMilestone component used to listen
BV_DROPDOWN_HIDE event to collapse BoardEditableItem.

This MR instead attaches listens for @hide event
on EpicSelect to collapse BoardEditableItem.
(EpicSelect wraps GlDropdown and GlDropdown
normally exposes @hide and @hidden by default).
parent 91dc753a
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import projectMilestones from '../../graphql/project_milestones.query.graphql'; import projectMilestones from '../../graphql/project_milestones.query.graphql';
...@@ -73,21 +72,20 @@ export default { ...@@ -73,21 +72,20 @@ export default {
return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone; return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone;
}, },
}, },
mounted() {
this.$root.$on(BV_DROPDOWN_HIDE, () => {
this.$refs.sidebarItem.collapse();
});
},
methods: { methods: {
...mapActions(['setActiveIssueMilestone']), ...mapActions(['setActiveIssueMilestone']),
handleOpen() { handleOpen() {
this.edit = true; this.edit = true;
this.$refs.dropdown.show(); this.$refs.dropdown.show();
}, },
handleClose() {
this.edit = false;
this.$refs.sidebarItem.collapse();
},
async setMilestone(milestoneId) { async setMilestone(milestoneId) {
this.loading = true; this.loading = true;
this.searchTitle = ''; this.searchTitle = '';
this.$refs.sidebarItem.collapse(); this.handleClose();
try { try {
const input = { milestoneId, projectPath: this.projectPath }; const input = { milestoneId, projectPath: this.projectPath };
...@@ -116,7 +114,7 @@ export default { ...@@ -116,7 +114,7 @@ export default {
:title="$options.i18n.milestone" :title="$options.i18n.milestone"
:loading="loading" :loading="loading"
@open="handleOpen()" @open="handleOpen()"
@close="edit = false" @close="handleClose"
> >
<template v-if="hasMilestone" #collapsed> <template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong> <strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
...@@ -126,6 +124,7 @@ export default { ...@@ -126,6 +124,7 @@ export default {
:text="dropdownText" :text="dropdownText"
:header-text="$options.i18n.assignMilestone" :header-text="$options.i18n.assignMilestone"
block block
@hide="handleClose"
> >
<gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" /> <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
<gl-dropdown-item <gl-dropdown-item
......
...@@ -64,15 +64,25 @@ export default { ...@@ -64,15 +64,25 @@ export default {
}, },
methods: { methods: {
...mapActions(['setActiveIssueEpic', 'fetchEpicForActiveIssue']), ...mapActions(['setActiveIssueEpic', 'fetchEpicForActiveIssue']),
openEpicsDropdown() { handleOpen() {
if (!this.loading) { if (!this.epicFetchInProgress) {
this.$refs.epicSelect.handleEditClick(); this.$refs.epicSelect.toggleFormDropdown();
} else {
this.$refs.sidebarItem.collapse();
} }
}, },
async setEpic(selectedEpic) { handleClose() {
this.$refs.sidebarItem.collapse(); this.$refs.sidebarItem.collapse();
this.$refs.epicSelect.toggleFormDropdown();
},
async setEpic(selectedEpic) {
this.handleClose();
const epicId = selectedEpic?.id ? fullEpicId(selectedEpic.id) : null; const epicId = selectedEpic?.id ? fullEpicId(selectedEpic.id) : null;
const assignedEpicId = this.epic?.id ? fullEpicId(this.epic.id) : null;
if (epicId === assignedEpicId) {
return;
}
try { try {
await this.setActiveIssueEpic(epicId); await this.setActiveIssueEpic(epicId);
...@@ -89,7 +99,8 @@ export default { ...@@ -89,7 +99,8 @@ export default {
ref="sidebarItem" ref="sidebarItem"
:title="$options.i18n.epic" :title="$options.i18n.epic"
:loading="epicFetchInProgress" :loading="epicFetchInProgress"
@open="openEpicsDropdown" @open="handleOpen"
@close="handleClose"
> >
<template v-if="epicData.title" #collapsed> <template v-if="epicData.title" #collapsed>
<a class="gl-text-gray-900! gl-font-weight-bold" href="#"> <a class="gl-text-gray-900! gl-font-weight-bold" href="#">
...@@ -107,6 +118,7 @@ export default { ...@@ -107,6 +118,7 @@ export default {
variant="standalone" variant="standalone"
:show-header="false" :show-header="false"
@epicSelect="setEpic" @epicSelect="setEpic"
@hide="handleClose"
/> />
</board-editable-item> </board-editable-item>
</template> </template>
...@@ -199,6 +199,7 @@ export default { ...@@ -199,6 +199,7 @@ export default {
}, },
hideDropdown() { hideDropdown() {
this.isDropdownShowing = this.isDropdownVariantStandalone; this.isDropdownShowing = this.isDropdownVariantStandalone;
this.$emit('hide');
}, },
toggleFormDropdown() { toggleFormDropdown() {
const { dropdown } = this.$refs.dropdown.$refs; const { dropdown } = this.$refs.dropdown.$refs;
......
...@@ -27,7 +27,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -27,7 +27,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
wrapper = null; wrapper = null;
}); });
const fakeStore = ({ const createStore = ({
initialState = { initialState = {
activeId: mockIssueWithoutEpic.id, activeId: mockIssueWithoutEpic.id,
issues: { [mockIssueWithoutEpic.id]: { ...mockIssueWithoutEpic } }, issues: { [mockIssueWithoutEpic.id]: { ...mockIssueWithoutEpic } },
...@@ -59,7 +59,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -59,7 +59,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
BoardEditableItem, BoardEditableItem,
EpicsSelect: stubComponent(EpicsSelect, { EpicsSelect: stubComponent(EpicsSelect, {
methods: { methods: {
handleEditClick: epicsSelectHandleEditClick, toggleFormDropdown: epicsSelectHandleEditClick,
}, },
}), }),
}, },
...@@ -69,9 +69,43 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -69,9 +69,43 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
const findEpicSelect = () => wrapper.find({ ref: 'epicSelect' }); const findEpicSelect = () => wrapper.find({ ref: 'epicSelect' });
const findItemWrapper = () => wrapper.find({ ref: 'sidebarItem' }); const findItemWrapper = () => wrapper.find({ ref: 'sidebarItem' });
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
const findBoardEditableItem = () => wrapper.find(BoardEditableItem);
describe('when not editing', () => {
it('expands the milestone dropdown on clicking edit', async () => {
createStore();
createWrapper();
await findBoardEditableItem().vm.$emit('open');
expect(epicsSelectHandleEditClick).toHaveBeenCalled();
});
});
describe('when editing', () => {
beforeEach(() => {
createStore();
createWrapper();
findItemWrapper().vm.$emit('open');
jest.spyOn(wrapper.vm.$refs.sidebarItem, 'collapse');
});
it('collapses BoardEditableItem on clicking edit', async () => {
await findBoardEditableItem().vm.$emit('close');
expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
});
it('collapses BoardEditableItem on hiding dropdown', async () => {
await wrapper.find(EpicsSelect).vm.$emit('hide');
expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
});
});
it('renders "None" when no epic is assigned to the active issue', async () => { it('renders "None" when no epic is assigned to the active issue', async () => {
fakeStore(); createStore();
createWrapper(); createWrapper();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -83,7 +117,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -83,7 +117,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
it('fetches an epic for active issue', () => { it('fetches an epic for active issue', () => {
const fetchEpicForActiveIssue = jest.fn(() => Promise.resolve()); const fetchEpicForActiveIssue = jest.fn(() => Promise.resolve());
fakeStore({ createStore({
initialState: { initialState: {
activeId: mockIssueWithEpic.id, activeId: mockIssueWithEpic.id,
issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } }, issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } },
...@@ -101,7 +135,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -101,7 +135,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
}); });
it('flashes an error message when fetch fails', async () => { it('flashes an error message when fetch fails', async () => {
fakeStore({ createStore({
initialState: { initialState: {
activeId: mockIssueWithEpic.id, activeId: mockIssueWithEpic.id,
issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } }, issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } },
...@@ -126,7 +160,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -126,7 +160,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
}); });
it('renders epic title when issue has an assigned epic', async () => { it('renders epic title when issue has an assigned epic', async () => {
fakeStore({ createStore({
initialState: { initialState: {
activeId: mockIssueWithEpic.id, activeId: mockIssueWithEpic.id,
issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } }, issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } },
...@@ -143,18 +177,9 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -143,18 +177,9 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
}); });
}); });
it('expands the dropdown when editing', () => {
fakeStore();
createWrapper();
findItemWrapper().vm.$emit('open');
expect(epicsSelectHandleEditClick).toHaveBeenCalled();
});
describe('when epic is selected', () => { describe('when epic is selected', () => {
beforeEach(async () => { beforeEach(async () => {
fakeStore({ createStore({
initialState: { initialState: {
activeId: mockIssueWithoutEpic.id, activeId: mockIssueWithoutEpic.id,
issues: { [mockIssueWithoutEpic.id]: { ...mockIssueWithoutEpic } }, issues: { [mockIssueWithoutEpic.id]: { ...mockIssueWithoutEpic } },
...@@ -190,11 +215,25 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -190,11 +215,25 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
expect(findCollapsed().isVisible()).toBe(true); expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe(mockAssignedEpic.title); expect(findCollapsed().text()).toBe(mockAssignedEpic.title);
}); });
describe('when the selected epic did not change', () => {
it('does not commit change to the server', async () => {
createStore();
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueEpic').mockImplementation();
findEpicSelect().vm.$emit('epicSelect', null);
await wrapper.vm.$nextTick();
expect(wrapper.vm.setActiveIssueEpic).not.toHaveBeenCalled();
});
});
}); });
describe('when no epic is selected', () => { describe('when no epic is selected', () => {
beforeEach(async () => { beforeEach(async () => {
fakeStore({ createStore({
initialState: { initialState: {
activeId: mockIssueWithEpic.id, activeId: mockIssueWithEpic.id,
issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } }, issues: { [mockIssueWithEpic.id]: { ...mockIssueWithEpic } },
...@@ -226,7 +265,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -226,7 +265,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
}); });
it('flashes an error when update fails', async () => { it('flashes an error when update fails', async () => {
fakeStore({ createStore({
actionsMock: { actionsMock: {
setActiveIssueEpic: jest.fn().mockRejectedValue('mayday'), setActiveIssueEpic: jest.fn().mockRejectedValue('mayday'),
}, },
...@@ -234,7 +273,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => { ...@@ -234,7 +273,7 @@ describe('ee/boards/components/sidebar/board_sidebar_epic_select.vue', () => {
createWrapper(); createWrapper();
findEpicSelect().vm.$emit('epicSelect', null); findEpicSelect().vm.$emit('epicSelect', { id: 'foo' });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
......
...@@ -137,6 +137,12 @@ describe('EpicsSelect', () => { ...@@ -137,6 +137,12 @@ describe('EpicsSelect', () => {
expect(wrapperStandalone.vm.isDropdownShowing).toBe(true); expect(wrapperStandalone.vm.isDropdownShowing).toBe(true);
}); });
it('should emit `hide` event', () => {
wrapperStandalone.vm.hideDropdown();
expect(wrapperStandalone.emitted().hide.length).toBe(1);
});
}); });
describe('handleItemSelect', () => { describe('handleItemSelect', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlDropdown } from '@gitlab/ui';
import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data'; import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
...@@ -46,10 +46,42 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => ...@@ -46,10 +46,42 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
const findLoader = () => wrapper.find(GlLoadingIcon); const findLoader = () => wrapper.find(GlLoadingIcon);
const findDropdown = () => wrapper.find(GlDropdown);
const findBoardEditableItem = () => wrapper.find(BoardEditableItem);
const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]'); const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]');
const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]'); const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]');
const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]'); const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]');
describe('when not editing', () => {
it('opens the milestone dropdown on clicking edit', async () => {
createWrapper();
wrapper.vm.$refs.dropdown.show = jest.fn();
await findBoardEditableItem().vm.$emit('open');
expect(wrapper.vm.$refs.dropdown.show).toHaveBeenCalledTimes(1);
});
});
describe('when editing', () => {
beforeEach(() => {
createWrapper();
jest.spyOn(wrapper.vm.$refs.sidebarItem, 'collapse');
});
it('collapses BoardEditableItem on clicking edit', async () => {
await findBoardEditableItem().vm.$emit('close');
expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
});
it('collapses BoardEditableItem on hiding dropdown', async () => {
await findDropdown().vm.$emit('hide');
expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
});
});
it('renders "None" when no milestone is selected', () => { it('renders "None" when no milestone is selected', () => {
createWrapper(); createWrapper();
......
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