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 {
: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