Commit 41b9d34c authored by Phil Hughes's avatar Phil Hughes

Merge branch '13847-add-variant-support-epics-dropdown' into 'master'

Add variant support for Epics dropdown

See merge request gitlab-org/gitlab!32544
parents 791bdade 22b8c5b7
...@@ -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