Commit 22b8c5b7 authored by Kushal Pandya's avatar Kushal Pandya

Add variant support for Epics dropdown

Adds variant support for Epics dropdown such that it can be
used in sidebar as well as a standalone dropdown.
parent 3b36620c
...@@ -88,7 +88,6 @@ export default { ...@@ -88,7 +88,6 @@ export default {
:can-edit="canEdit" :can-edit="canEdit"
:initial-epic="epic" :initial-epic="epic"
:initial-epic-loading="initialEpicLoading" :initial-epic-loading="initialEpicLoading"
:block-title="__('Epic')"
> >
{{ __('None') }} {{ __('None') }}
</epics-select> </epics-select>
......
...@@ -4,6 +4,7 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -4,6 +4,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import $ from 'jquery'; import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { noneEpic } from 'ee/vue_shared/constants'; import { noneEpic } from 'ee/vue_shared/constants';
import createStore from './store'; import createStore from './store';
...@@ -17,6 +18,8 @@ import DropdownHeader from './dropdown_header.vue'; ...@@ -17,6 +18,8 @@ import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue'; import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
import { DropdownVariant } from './constants';
export default { export default {
store: createStore(), store: createStore(),
components: { components: {
...@@ -48,7 +51,8 @@ export default { ...@@ -48,7 +51,8 @@ export default {
}, },
blockTitle: { blockTitle: {
type: String, type: String,
required: true, required: false,
default: __('Epic'),
}, },
initialEpic: { initialEpic: {
type: Object, type: Object,
...@@ -58,18 +62,26 @@ export default { ...@@ -58,18 +62,26 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
variant: {
type: String,
required: false,
default: DropdownVariant.Sidebar,
},
}, },
data() { data() {
return { return {
showDropdown: false, showDropdown: this.variant === DropdownVariant.Standalone,
}; };
}, },
computed: { computed: {
...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic', 'searchQuery']), ...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic', 'searchQuery']),
...mapGetters(['groupEpics']), ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone', 'groupEpics']),
dropdownSelectInProgress() { dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress; return this.initialEpicLoading || this.epicSelectInProgress;
}, },
dropdownButtonTextClass() {
return { 'is-default': this.isDropdownVariantStandalone };
},
}, },
watch: { watch: {
/** /**
...@@ -109,6 +121,7 @@ export default { ...@@ -109,6 +121,7 @@ export default {
}, },
mounted() { mounted() {
this.setInitialData({ this.setInitialData({
variant: this.variant,
groupId: this.groupId, groupId: this.groupId,
issueId: this.issueId, issueId: this.issueId,
selectedEpic: this.selectedEpic, selectedEpic: this.selectedEpic,
...@@ -143,13 +156,15 @@ export default { ...@@ -143,13 +156,15 @@ export default {
}); });
}, },
handleDropdownHidden() { handleDropdownHidden() {
this.showDropdown = false; this.showDropdown = this.isDropdownVariantStandalone;
}, },
handleItemSelect(epic) { handleItemSelect(epic) {
if (epic.id === noneEpic.id && epic.title === noneEpic.title) { if (this.epicIssueId && epic.id === noneEpic.id && epic.title === noneEpic.title) {
this.removeIssueFromEpic(this.selectedEpic); this.removeIssueFromEpic(this.selectedEpic);
} else { } else if (this.issueId) {
this.assignIssueToEpic(epic); this.assignIssueToEpic(epic);
} else {
this.$emit('onEpicSelect', epic);
} }
}, },
}, },
...@@ -157,25 +172,31 @@ export default { ...@@ -157,25 +172,31 @@ export default {
</script> </script>
<template> <template>
<div class="block epic js-epic-block"> <div class="js-epic-block" :class="{ 'block epic': isDropdownVariantSidebar }">
<dropdown-value-collapsed :epic="selectedEpic" /> <dropdown-value-collapsed v-if="isDropdownVariantSidebar" :epic="selectedEpic" />
<dropdown-title <dropdown-title
v-if="isDropdownVariantSidebar"
:can-edit="canEdit" :can-edit="canEdit"
:block-title="blockTitle" :block-title="blockTitle"
:is-loading="dropdownSelectInProgress" :is-loading="dropdownSelectInProgress"
@onClickEdit="handleEditClick" @onClickEdit="handleEditClick"
/> />
<dropdown-value v-show="!showDropdown" :epic="selectedEpic"> <dropdown-value v-if="isDropdownVariantSidebar" v-show="!showDropdown" :epic="selectedEpic">
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<div v-if="canEdit" v-show="showDropdown" class="epic-dropdown-container">
<div ref="dropdown" class="dropdown">
<dropdown-button ref="dropdownButton" />
<div <div
class="dropdown-menu dropdown-select v-if="canEdit || isDropdownVariantStandalone"
dropdown-menu-epics dropdown-menu-selectable" v-show="showDropdown"
class="epic-dropdown-container"
> >
<dropdown-header /> <div ref="dropdown" class="dropdown">
<dropdown-button
ref="dropdownButton"
:selected-epic-title="selectedEpic.title"
:toggle-text-class="dropdownButtonTextClass"
/>
<div class="dropdown-menu dropdown-select dropdown-menu-epics dropdown-menu-selectable">
<dropdown-header v-if="isDropdownVariantSidebar" />
<dropdown-search-input @onSearchInput="setSearchQuery" /> <dropdown-search-input @onSearchInput="setSearchQuery" />
<dropdown-contents <dropdown-contents
v-if="!epicsFetchInProgress" v-if="!epicsFetchInProgress"
......
// eslint-disable-next-line import/prefer-default-export
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
};
<script> <script>
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
Icon, Icon,
}, },
props: {
selectedEpicTitle: {
type: String,
required: false,
default: '',
},
toggleTextClass: {
type: Object,
required: false,
default: null,
},
},
computed: {
buttonText() {
return this.selectedEpicTitle || __('Epic');
},
},
}; };
</script> </script>
...@@ -15,7 +33,7 @@ export default { ...@@ -15,7 +33,7 @@ export default {
data-display="static" data-display="static"
data-toggle="dropdown" data-toggle="dropdown"
> >
<span class="dropdown-toggle-text">{{ __('Epic') }}</span> <span class="dropdown-toggle-text" :class="toggleTextClass">{{ buttonText }}</span>
<icon name="chevron-down" /> <icon name="chevron-down" />
</button> </button>
</template> </template>
import { searchBy } from '~/lib/utils/common_utils'; import { searchBy } from '~/lib/utils/common_utils';
import { DropdownVariant } from '../constants';
/** /**
* Returns array of Epics * Returns array of Epics
...@@ -31,5 +32,19 @@ export const groupEpics = state => { ...@@ -31,5 +32,19 @@ export const groupEpics = state => {
return state.epics; return state.epics;
}; };
/**
* Returns boolean representing whether dropdown variant
* is `sidebar`
* @param {object} state
*/
export const isDropdownVariantSidebar = state => state.variant === DropdownVariant.Sidebar;
/**
* Returns boolean representing whether dropdown variant
* is `standalone`
* @param {object} state
*/
export const isDropdownVariantStandalone = state => state.variant === DropdownVariant.Standalone;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_INITIAL_DATA](state, { groupId, issueId, selectedEpic, selectedEpicIssueId }) { [types.SET_INITIAL_DATA](
state,
{ variant, groupId, issueId, selectedEpic, selectedEpicIssueId },
) {
state.variant = variant;
state.groupId = groupId; state.groupId = groupId;
state.issueId = issueId; state.issueId = issueId;
state.selectedEpic = selectedEpic; state.selectedEpic = selectedEpic;
......
...@@ -10,6 +10,7 @@ export default () => ({ ...@@ -10,6 +10,7 @@ export default () => ({
epics: [], epics: [],
// UI Flags // UI Flags
variant: '',
epicSelectInProgress: false, epicSelectInProgress: false,
epicsFetchInProgress: false, epicsFetchInProgress: false,
}); });
...@@ -12,33 +12,68 @@ import DropdownSearchInput from 'ee/vue_shared/components/sidebar/epics_select/d ...@@ -12,33 +12,68 @@ import DropdownSearchInput from 'ee/vue_shared/components/sidebar/epics_select/d
import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue'; import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue';
import createDefaultStore from 'ee/vue_shared/components/sidebar/epics_select/store'; import createDefaultStore from 'ee/vue_shared/components/sidebar/epics_select/store';
import { DropdownVariant } from 'ee/vue_shared/components/sidebar/epics_select//constants';
import { mockEpic1, mockEpic2, mockAssignRemoveRes, mockIssue, noneEpic } from '../mock_data'; import { mockEpic1, mockEpic2, mockAssignRemoveRes, mockIssue, noneEpic } from '../mock_data';
describe('EpicsSelect', () => { describe('EpicsSelect', () => {
describe('Base', () => { describe('Base', () => {
let wrapper; let wrapper;
let wrapperStandalone;
// const errorMessage = 'Something went wrong while fetching group epics.'; // const errorMessage = 'Something went wrong while fetching group epics.';
const store = createDefaultStore(); const store = createDefaultStore();
const storeStandalone = createDefaultStore();
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
wrapper = shallowMount(EpicsSelectBase, { const props = {
store,
propsData: {
canEdit: true, canEdit: true,
blockTitle: 'Epic',
initialEpic: mockEpic1, initialEpic: mockEpic1,
initialEpicLoading: false, initialEpicLoading: false,
epicIssueId: mockIssue.epic_issue_id, epicIssueId: mockIssue.epic_issue_id,
groupId: mockEpic1.group_id, groupId: mockEpic1.group_id,
issueId: mockIssue.id, issueId: mockIssue.id,
};
wrapper = shallowMount(EpicsSelectBase, {
store,
propsData: {
...props,
},
});
wrapperStandalone = shallowMount(EpicsSelectBase, {
store: storeStandalone,
propsData: {
...props,
variant: DropdownVariant.Standalone,
}, },
}); });
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapperStandalone.destroy();
});
describe('computed', () => {
describe('dropdownButtonTextClass', () => {
it('should return object { is-default: true } when variant is "standalone"', () => {
expect(wrapperStandalone.vm.dropdownButtonTextClass).toEqual(
expect.objectContaining({
'is-default': true,
}),
);
});
it('should return object { is-default: false } when variant is "sidebar"', () => {
expect(wrapper.vm.dropdownButtonTextClass).toEqual(
expect.objectContaining({
'is-default': false,
}),
);
});
});
}); });
describe('watchers', () => { describe('watchers', () => {
...@@ -106,10 +141,16 @@ describe('EpicsSelect', () => { ...@@ -106,10 +141,16 @@ describe('EpicsSelect', () => {
expect(wrapper.vm.showDropdown).toBe(false); expect(wrapper.vm.showDropdown).toBe(false);
}); });
it('should set `showDropdown` to true when dropdown variant is "standalone"', () => {
wrapperStandalone.vm.handleDropdownHidden();
expect(wrapperStandalone.vm.showDropdown).toBe(true);
});
}); });
describe('handleItemSelect', () => { describe('handleItemSelect', () => {
it('should call `removeIssueFromEpic` with selected epic when `epic` param represents `No Epic`', () => { it('should call `removeIssueFromEpic` with selected epic when `epic` param represents `No Epic` and `epicIssueId` is defined', () => {
jest.spyOn(wrapper.vm, 'removeIssueFromEpic').mockReturnValue( jest.spyOn(wrapper.vm, 'removeIssueFromEpic').mockReturnValue(
Promise.resolve({ Promise.resolve({
data: mockAssignRemoveRes, data: mockAssignRemoveRes,
...@@ -122,7 +163,7 @@ describe('EpicsSelect', () => { ...@@ -122,7 +163,7 @@ describe('EpicsSelect', () => {
expect(wrapper.vm.removeIssueFromEpic).toHaveBeenCalledWith(mockEpic1); expect(wrapper.vm.removeIssueFromEpic).toHaveBeenCalledWith(mockEpic1);
}); });
it('should call `assignIssueToEpic` with passed `epic` param when it does not represent `No Epic`', () => { it('should call `assignIssueToEpic` with passed `epic` param when it does not represent `No Epic` and `issueId` prop is defined', () => {
jest.spyOn(wrapper.vm, 'assignIssueToEpic').mockReturnValue( jest.spyOn(wrapper.vm, 'assignIssueToEpic').mockReturnValue(
Promise.resolve({ Promise.resolve({
data: mockAssignRemoveRes, data: mockAssignRemoveRes,
...@@ -133,31 +174,55 @@ describe('EpicsSelect', () => { ...@@ -133,31 +174,55 @@ describe('EpicsSelect', () => {
expect(wrapper.vm.assignIssueToEpic).toHaveBeenCalledWith(mockEpic2); expect(wrapper.vm.assignIssueToEpic).toHaveBeenCalledWith(mockEpic2);
}); });
it('should emit component event `onEpicSelect` with both `epicIssueId` & `issueId` props are not defined', () => {
wrapperStandalone.setProps({
issueId: 0,
epicIssueId: 0,
});
return wrapperStandalone.vm.$nextTick(() => {
wrapperStandalone.vm.handleItemSelect(mockEpic2);
expect(wrapperStandalone.emitted('onEpicSelect')).toBeTruthy();
expect(wrapperStandalone.emitted('onEpicSelect')[0]).toEqual([mockEpic2]);
});
});
}); });
}); });
describe('template', () => { describe('template', () => {
const showDropdown = () => { const showDropdown = (w = wrapper) => {
wrapper.setProps({ w.setProps({
canEdit: true, canEdit: true,
}); });
wrapper.setData({ w.setData({
showDropdown: true, showDropdown: true,
}); });
}; };
it('should render component container element', () => { it('should render component container element', () => {
expect(wrapper.classes()).toContain('js-epic-block'); expect(wrapper.classes()).toEqual(['js-epic-block', 'block', 'epic']);
expect(wrapperStandalone.classes()).toEqual(['js-epic-block']);
}); });
it('should render DropdownValueCollapsed component', () => { it('should render DropdownValueCollapsed component', () => {
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
}); });
it('should not render DropdownValueCollapsed component when variant is "standalone"', () => {
expect(wrapperStandalone.find(DropdownValueCollapsed).exists()).toBe(false);
});
it('should render DropdownTitle component', () => { it('should render DropdownTitle component', () => {
expect(wrapper.find(DropdownTitle).exists()).toBe(true); expect(wrapper.find(DropdownTitle).exists()).toBe(true);
}); });
it('should not render DropdownTitle component when variant is "standalone"', () => {
expect(wrapperStandalone.find(DropdownTitle).exists()).toBe(false);
});
it('should render DropdownValue component when `showDropdown` is false', done => { it('should render DropdownValue component when `showDropdown` is false', done => {
wrapper.vm.showDropdown = false; wrapper.vm.showDropdown = false;
...@@ -167,6 +232,10 @@ describe('EpicsSelect', () => { ...@@ -167,6 +232,10 @@ describe('EpicsSelect', () => {
}); });
}); });
it('should not render DropdownValue component when variant is "standalone"', () => {
expect(wrapperStandalone.find(DropdownValue).exists()).toBe(false);
});
it('should render dropdown container element when props `canEdit` & `showDropdown` are true', done => { it('should render dropdown container element when props `canEdit` & `showDropdown` are true', done => {
showDropdown(); showDropdown();
...@@ -177,6 +246,10 @@ describe('EpicsSelect', () => { ...@@ -177,6 +246,10 @@ describe('EpicsSelect', () => {
}); });
}); });
it('should render dropdown container element when variant is "standalone"', () => {
expect(wrapperStandalone.find('.epic-dropdown-container').exists()).toBe(true);
});
it('should render DropdownButton component when props `canEdit` & `showDropdown` are true', done => { it('should render DropdownButton component when props `canEdit` & `showDropdown` are true', done => {
showDropdown(); showDropdown();
...@@ -204,6 +277,14 @@ describe('EpicsSelect', () => { ...@@ -204,6 +277,14 @@ describe('EpicsSelect', () => {
}); });
}); });
it('should not render DropdownHeader component when variant is "standalone"', () => {
showDropdown(wrapperStandalone);
return wrapperStandalone.vm.$nextTick(() => {
expect(wrapperStandalone.find(DropdownHeader).exists()).toBe(false);
});
});
it('should render DropdownSearchInput component when props `canEdit` & `showDropdown` are true', done => { it('should render DropdownSearchInput component when props `canEdit` & `showDropdown` are true', done => {
showDropdown(); showDropdown();
......
...@@ -3,16 +3,37 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,16 +3,37 @@ import { shallowMount } from '@vue/test-utils';
import DropdownButton from 'ee/vue_shared/components/sidebar/epics_select/dropdown_button.vue'; import DropdownButton from 'ee/vue_shared/components/sidebar/epics_select/dropdown_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { mockEpic1 } from '../mock_data';
describe('EpicsSelect', () => { describe('EpicsSelect', () => {
describe('DropdownButton', () => { describe('DropdownButton', () => {
let wrapper; let wrapper;
let wrapperWithEpic;
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(DropdownButton); wrapper = shallowMount(DropdownButton);
wrapperWithEpic = shallowMount(DropdownButton, {
propsData: {
selectedEpicTitle: mockEpic1.title,
},
});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapperWithEpic.destroy();
});
describe('computed', () => {
describe('buttonText', () => {
it('returns string "Epic" when `selectedEpicTitle` prop is empty', () => {
expect(wrapper.vm.buttonText).toBe('Epic');
});
it('returns string containing `selectedEpicTitle`', () => {
expect(wrapperWithEpic.vm.buttonText).toBe(mockEpic1.title);
});
});
}); });
describe('template', () => { describe('template', () => {
...@@ -30,6 +51,21 @@ describe('EpicsSelect', () => { ...@@ -30,6 +51,21 @@ describe('EpicsSelect', () => {
expect(titleEl.exists()).toBe(true); expect(titleEl.exists()).toBe(true);
expect(titleEl.text()).toBe('Epic'); expect(titleEl.text()).toBe('Epic');
const titleWithEpicEl = wrapperWithEpic.find('.dropdown-toggle-text');
expect(titleWithEpicEl.exists()).toBe(true);
expect(titleWithEpicEl.text()).toBe(mockEpic1.title);
});
it('should render button title with toggleTextClass prop value', () => {
wrapper.setProps({
toggleTextClass: { 'is-default': true },
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.dropdown-toggle-text').classes()).toContain('is-default');
});
}); });
it('should render Icon component', () => { it('should render Icon component', () => {
......
...@@ -86,6 +86,18 @@ describe('EpicsSelect', () => { ...@@ -86,6 +86,18 @@ describe('EpicsSelect', () => {
); );
}); });
}); });
describe('isDropdownVariantSidebar', () => {
it('returns `true` when `state.variant` is "sidebar"', () => {
expect(getters.isDropdownVariantSidebar({ variant: 'sidebar' })).toBe(true);
});
});
describe('isDropdownVariantStandalone', () => {
it('returns `true` when `state.variant` is "standalone"', () => {
expect(getters.isDropdownVariantStandalone({ variant: 'standalone' })).toBe(true);
});
});
}); });
}); });
}); });
...@@ -2,6 +2,7 @@ import mutations from 'ee/vue_shared/components/sidebar/epics_select/store/mutat ...@@ -2,6 +2,7 @@ import mutations from 'ee/vue_shared/components/sidebar/epics_select/store/mutat
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state'; import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/mutation_types'; import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/mutation_types';
import { DropdownVariant } from 'ee/vue_shared/components/sidebar/epics_select//constants';
import { mockEpic1, mockIssue } from '../../mock_data'; import { mockEpic1, mockIssue } from '../../mock_data';
...@@ -17,6 +18,7 @@ describe('EpicsSelect', () => { ...@@ -17,6 +18,7 @@ describe('EpicsSelect', () => {
describe(types.SET_INITIAL_DATA, () => { describe(types.SET_INITIAL_DATA, () => {
it('should set provided `data` param props to state', () => { it('should set provided `data` param props to state', () => {
const data = { const data = {
variant: DropdownVariant.Sidebar,
groupId: mockEpic1.group_id, groupId: mockEpic1.group_id,
issueId: mockIssue.id, issueId: mockIssue.id,
selectedEpic: mockEpic1, selectedEpic: mockEpic1,
...@@ -25,6 +27,7 @@ describe('EpicsSelect', () => { ...@@ -25,6 +27,7 @@ describe('EpicsSelect', () => {
mutations[types.SET_INITIAL_DATA](state, data); mutations[types.SET_INITIAL_DATA](state, data);
expect(state).toHaveProperty('variant', data.variant);
expect(state).toHaveProperty('groupId', data.groupId); expect(state).toHaveProperty('groupId', data.groupId);
expect(state).toHaveProperty('issueId', data.issueId); expect(state).toHaveProperty('issueId', data.issueId);
expect(state).toHaveProperty('selectedEpic', data.selectedEpic); expect(state).toHaveProperty('selectedEpic', data.selectedEpic);
......
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