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 {
:can-edit="canEdit"
:initial-epic="epic"
:initial-epic-loading="initialEpicLoading"
:block-title="__('Epic')"
>
{{ __('None') }}
</epics-select>
......
......@@ -4,6 +4,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { noneEpic } from 'ee/vue_shared/constants';
import createStore from './store';
......@@ -17,6 +18,8 @@ import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownContents from './dropdown_contents.vue';
import { DropdownVariant } from './constants';
export default {
store: createStore(),
components: {
......@@ -48,7 +51,8 @@ export default {
},
blockTitle: {
type: String,
required: true,
required: false,
default: __('Epic'),
},
initialEpic: {
type: Object,
......@@ -58,18 +62,26 @@ export default {
type: Boolean,
required: true,
},
variant: {
type: String,
required: false,
default: DropdownVariant.Sidebar,
},
},
data() {
return {
showDropdown: false,
showDropdown: this.variant === DropdownVariant.Standalone,
};
},
computed: {
...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic', 'searchQuery']),
...mapGetters(['groupEpics']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone', 'groupEpics']),
dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress;
},
dropdownButtonTextClass() {
return { 'is-default': this.isDropdownVariantStandalone };
},
},
watch: {
/**
......@@ -109,6 +121,7 @@ export default {
},
mounted() {
this.setInitialData({
variant: this.variant,
groupId: this.groupId,
issueId: this.issueId,
selectedEpic: this.selectedEpic,
......@@ -143,13 +156,15 @@ export default {
});
},
handleDropdownHidden() {
this.showDropdown = false;
this.showDropdown = this.isDropdownVariantStandalone;
},
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);
} else {
} else if (this.issueId) {
this.assignIssueToEpic(epic);
} else {
this.$emit('onEpicSelect', epic);
}
},
},
......@@ -157,25 +172,31 @@ export default {
</script>
<template>
<div class="block epic js-epic-block">
<dropdown-value-collapsed :epic="selectedEpic" />
<div class="js-epic-block" :class="{ 'block epic': isDropdownVariantSidebar }">
<dropdown-value-collapsed v-if="isDropdownVariantSidebar" :epic="selectedEpic" />
<dropdown-title
v-if="isDropdownVariantSidebar"
:can-edit="canEdit"
:block-title="blockTitle"
:is-loading="dropdownSelectInProgress"
@onClickEdit="handleEditClick"
/>
<dropdown-value v-show="!showDropdown" :epic="selectedEpic">
<dropdown-value v-if="isDropdownVariantSidebar" v-show="!showDropdown" :epic="selectedEpic">
<slot></slot>
</dropdown-value>
<div v-if="canEdit" v-show="showDropdown" class="epic-dropdown-container">
<div
v-if="canEdit || isDropdownVariantStandalone"
v-show="showDropdown"
class="epic-dropdown-container"
>
<div ref="dropdown" class="dropdown">
<dropdown-button ref="dropdownButton" />
<div
class="dropdown-menu dropdown-select
dropdown-menu-epics dropdown-menu-selectable"
>
<dropdown-header />
<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-contents
v-if="!epicsFetchInProgress"
......
// eslint-disable-next-line import/prefer-default-export
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
};
<script>
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
selectedEpicTitle: {
type: String,
required: false,
default: '',
},
toggleTextClass: {
type: Object,
required: false,
default: null,
},
},
computed: {
buttonText() {
return this.selectedEpicTitle || __('Epic');
},
},
};
</script>
......@@ -15,7 +33,7 @@ export default {
data-display="static"
data-toggle="dropdown"
>
<span class="dropdown-toggle-text">{{ __('Epic') }}</span>
<span class="dropdown-toggle-text" :class="toggleTextClass">{{ buttonText }}</span>
<icon name="chevron-down" />
</button>
</template>
import { searchBy } from '~/lib/utils/common_utils';
import { DropdownVariant } from '../constants';
/**
* Returns array of Epics
......@@ -31,5 +32,19 @@ export const groupEpics = state => {
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
export default () => {};
import * as types from './mutation_types';
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.issueId = issueId;
state.selectedEpic = selectedEpic;
......
......@@ -10,6 +10,7 @@ export default () => ({
epics: [],
// UI Flags
variant: '',
epicSelectInProgress: false,
epicsFetchInProgress: false,
});
......@@ -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 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';
describe('EpicsSelect', () => {
describe('Base', () => {
let wrapper;
let wrapperStandalone;
// const errorMessage = 'Something went wrong while fetching group epics.';
const store = createDefaultStore();
const storeStandalone = createDefaultStore();
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
const props = {
canEdit: true,
initialEpic: mockEpic1,
initialEpicLoading: false,
epicIssueId: mockIssue.epic_issue_id,
groupId: mockEpic1.group_id,
issueId: mockIssue.id,
};
wrapper = shallowMount(EpicsSelectBase, {
store,
propsData: {
canEdit: true,
blockTitle: 'Epic',
initialEpic: mockEpic1,
initialEpicLoading: false,
epicIssueId: mockIssue.epic_issue_id,
groupId: mockEpic1.group_id,
issueId: mockIssue.id,
...props,
},
});
wrapperStandalone = shallowMount(EpicsSelectBase, {
store: storeStandalone,
propsData: {
...props,
variant: DropdownVariant.Standalone,
},
});
});
afterEach(() => {
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', () => {
......@@ -106,10 +141,16 @@ describe('EpicsSelect', () => {
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', () => {
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(
Promise.resolve({
data: mockAssignRemoveRes,
......@@ -122,7 +163,7 @@ describe('EpicsSelect', () => {
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(
Promise.resolve({
data: mockAssignRemoveRes,
......@@ -133,31 +174,55 @@ describe('EpicsSelect', () => {
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', () => {
const showDropdown = () => {
wrapper.setProps({
const showDropdown = (w = wrapper) => {
w.setProps({
canEdit: true,
});
wrapper.setData({
w.setData({
showDropdown: true,
});
};
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', () => {
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', () => {
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 => {
wrapper.vm.showDropdown = false;
......@@ -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 => {
showDropdown();
......@@ -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 => {
showDropdown();
......@@ -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 => {
showDropdown();
......
......@@ -3,16 +3,37 @@ import { shallowMount } from '@vue/test-utils';
import DropdownButton from 'ee/vue_shared/components/sidebar/epics_select/dropdown_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { mockEpic1 } from '../mock_data';
describe('EpicsSelect', () => {
describe('DropdownButton', () => {
let wrapper;
let wrapperWithEpic;
beforeEach(() => {
wrapper = shallowMount(DropdownButton);
wrapperWithEpic = shallowMount(DropdownButton, {
propsData: {
selectedEpicTitle: mockEpic1.title,
},
});
});
afterEach(() => {
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', () => {
......@@ -30,6 +51,21 @@ describe('EpicsSelect', () => {
expect(titleEl.exists()).toBe(true);
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', () => {
......
......@@ -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
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 { DropdownVariant } from 'ee/vue_shared/components/sidebar/epics_select//constants';
import { mockEpic1, mockIssue } from '../../mock_data';
......@@ -17,6 +18,7 @@ describe('EpicsSelect', () => {
describe(types.SET_INITIAL_DATA, () => {
it('should set provided `data` param props to state', () => {
const data = {
variant: DropdownVariant.Sidebar,
groupId: mockEpic1.group_id,
issueId: mockIssue.id,
selectedEpic: mockEpic1,
......@@ -25,6 +27,7 @@ describe('EpicsSelect', () => {
mutations[types.SET_INITIAL_DATA](state, data);
expect(state).toHaveProperty('variant', data.variant);
expect(state).toHaveProperty('groupId', data.groupId);
expect(state).toHaveProperty('issueId', data.issueId);
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