Commit f8ca9879 authored by Kushal Pandya's avatar Kushal Pandya

Add variants support in labels dropdown

Adds variant support in Vue Labels Dropdown, currently it
supports `sidebar` & `standalone` variants.
parent 972c347a
// eslint-disable-next-line import/prefer-default-export
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
};
<script> <script>
import { mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { GlDeprecatedButton, GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon } from '@gitlab/ui';
export default { export default {
components: { components: {
GlDeprecatedButton, GlButton,
GlIcon, GlIcon,
}, },
computed: { computed: {
...mapGetters(['dropdownButtonText']), ...mapGetters(['dropdownButtonText', 'isDropdownVariantStandalone']),
},
methods: {
...mapActions(['toggleDropdownContents']),
handleButtonClick(e) {
if (this.isDropdownVariantStandalone) {
this.toggleDropdownContents();
e.stopPropagation();
}
},
}, },
}; };
</script> </script>
<template> <template>
<gl-deprecated-button class="labels-select-dropdown-button w-100 text-left"> <gl-button
<span class="dropdown-toggle-text">{{ dropdownButtonText }}</span> class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
@click="handleButtonClick"
>
<span class="dropdown-toggle-text" :class="{ 'flex-fill': isDropdownVariantStandalone }">{{
dropdownButtonText
}}</span>
<gl-icon name="chevron-down" class="pull-right" /> <gl-icon name="chevron-down" class="pull-right" />
</gl-deprecated-button> </gl-button>
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
GlTooltipDirective,
GlDeprecatedButton,
GlIcon,
GlFormInput,
GlLink,
GlLoadingIcon,
} from '@gitlab/ui';
export default { export default {
components: { components: {
GlDeprecatedButton, GlButton,
GlIcon,
GlFormInput, GlFormInput,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
...@@ -60,25 +52,23 @@ export default { ...@@ -60,25 +52,23 @@ export default {
<template> <template>
<div class="labels-select-contents-create js-labels-create"> <div class="labels-select-contents-create js-labels-create">
<div class="dropdown-title d-flex align-items-center pt-0 pb-2"> <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
<gl-deprecated-button <gl-button
:aria-label="__('Go back')" :aria-label="__('Go back')"
variant="link" variant="link"
size="sm" size="small"
class="js-btn-back dropdown-header-button p-0" class="js-btn-back dropdown-header-button p-0"
icon="arrow-left"
@click="toggleDropdownContentsCreateView" @click="toggleDropdownContentsCreateView"
> />
<gl-icon name="arrow-left" />
</gl-deprecated-button>
<span class="flex-grow-1">{{ labelsCreateTitle }}</span> <span class="flex-grow-1">{{ labelsCreateTitle }}</span>
<gl-deprecated-button <gl-button
:aria-label="__('Close')" :aria-label="__('Close')"
variant="link" variant="link"
size="sm" size="small"
class="dropdown-header-button p-0" class="dropdown-header-button p-0"
icon="close"
@click="toggleDropdownContents" @click="toggleDropdownContents"
> />
<gl-icon name="close" />
</gl-deprecated-button>
</div> </div>
<div class="dropdown-input"> <div class="dropdown-input">
<gl-form-input <gl-form-input
...@@ -107,21 +97,19 @@ export default { ...@@ -107,21 +97,19 @@ export default {
</div> </div>
</div> </div>
<div class="dropdown-actions clearfix pt-2 px-2"> <div class="dropdown-actions clearfix pt-2 px-2">
<gl-deprecated-button <gl-button
:disabled="disableCreate" :disabled="disableCreate"
variant="primary" category="primary"
variant="success"
class="pull-left d-flex align-items-center" class="pull-left d-flex align-items-center"
@click="handleCreateClick" @click="handleCreateClick"
> >
<gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" /> <gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
{{ __('Create') }} {{ __('Create') }}
</gl-deprecated-button> </gl-button>
<gl-deprecated-button <gl-button class="pull-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView">
class="pull-right js-btn-cancel-create"
@click="toggleDropdownContentsCreateView"
>
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-deprecated-button> </gl-button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlDeprecatedButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlDeprecatedButton, GlButton,
GlIcon, GlIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlLink, GlLink,
...@@ -20,6 +20,8 @@ export default { ...@@ -20,6 +20,8 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState([
'allowLabelCreate',
'allowMultiselect',
'labelsManagePath', 'labelsManagePath',
'labels', 'labels',
'labelsFetchInProgress', 'labelsFetchInProgress',
...@@ -27,7 +29,7 @@ export default { ...@@ -27,7 +29,7 @@ export default {
'footerCreateLabelTitle', 'footerCreateLabelTitle',
'footerManageLabelTitle', 'footerManageLabelTitle',
]), ]),
...mapGetters(['selectedLabelsList']), ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar']),
visibleLabels() { visibleLabels() {
if (this.searchKey) { if (this.searchKey) {
return this.labels.filter(label => return this.labels.filter(label =>
...@@ -56,6 +58,7 @@ export default { ...@@ -56,6 +58,7 @@ export default {
'toggleDropdownContentsCreateView', 'toggleDropdownContentsCreateView',
'fetchLabels', 'fetchLabels',
'updateSelectedLabels', 'updateSelectedLabels',
'toggleDropdownContents',
]), ]),
getDropdownLabelBoxStyle(label) { getDropdownLabelBoxStyle(label) {
return { return {
...@@ -111,6 +114,7 @@ export default { ...@@ -111,6 +114,7 @@ export default {
}, },
handleLabelClick(label) { handleLabelClick(label) {
this.updateSelectedLabels([label]); this.updateSelectedLabels([label]);
if (!this.allowMultiselect) this.toggleDropdownContents();
}, },
}, },
}; };
...@@ -123,17 +127,16 @@ export default { ...@@ -123,17 +127,16 @@ export default {
class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100" class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
size="md" size="md"
/> />
<div class="dropdown-title d-flex align-items-center pt-0 pb-2"> <div v-if="isDropdownVariantSidebar" class="dropdown-title d-flex align-items-center pt-0 pb-2">
<span class="flex-grow-1">{{ labelsListTitle }}</span> <span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-deprecated-button <gl-button
:aria-label="__('Close')" :aria-label="__('Close')"
variant="link" variant="link"
size="sm" size="small"
class="dropdown-header-button p-0" class="dropdown-header-button p-0"
icon="close"
@click="toggleDropdownContents" @click="toggleDropdownContents"
> />
<gl-icon name="close" />
</gl-deprecated-button>
</div> </div>
<div class="dropdown-input"> <div class="dropdown-input">
<gl-search-box-by-type v-model="searchKey" :autofocus="true" /> <gl-search-box-by-type v-model="searchKey" :autofocus="true" />
...@@ -157,14 +160,13 @@ export default { ...@@ -157,14 +160,13 @@ export default {
</li> </li>
</ul> </ul>
</div> </div>
<div class="dropdown-footer"> <div v-if="isDropdownVariantSidebar" class="dropdown-footer">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li> <li v-if="allowLabelCreate">
<gl-deprecated-button <gl-link
variant="link"
class="d-flex w-100 flex-row text-break-word label-item" class="d-flex w-100 flex-row text-break-word label-item"
@click="toggleDropdownContentsCreateView" @click="toggleDropdownContentsCreateView"
>{{ footerCreateLabelTitle }}</gl-deprecated-button >{{ footerCreateLabelTitle }}</gl-link
> >
</li> </li>
<li> <li>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import Vuex, { mapState, mapActions } from 'vuex'; import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
...@@ -13,6 +13,8 @@ import DropdownValue from './dropdown_value.vue'; ...@@ -13,6 +13,8 @@ import DropdownValue from './dropdown_value.vue';
import DropdownButton from './dropdown_button.vue'; import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
import { DropdownVariant } from './constants';
Vue.use(Vuex); Vue.use(Vuex);
export default { export default {
...@@ -33,14 +35,19 @@ export default { ...@@ -33,14 +35,19 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
allowMultiselect: {
type: Boolean,
required: false,
default: false,
},
allowScopedLabels: { allowScopedLabels: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
dropdownOnly: { variant: {
type: Boolean, type: String,
required: false, required: false,
default: false, default: DropdownVariant.Sidebar,
}, },
selectedLabels: { selectedLabels: {
type: Array, type: Array,
...@@ -90,6 +97,10 @@ export default { ...@@ -90,6 +97,10 @@ export default {
}, },
computed: { computed: {
...mapState(['showDropdownButton', 'showDropdownContents']), ...mapState(['showDropdownButton', 'showDropdownContents']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone']),
dropdownButtonVisible() {
return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
},
}, },
watch: { watch: {
selectedLabels(selectedLabels) { selectedLabels(selectedLabels) {
...@@ -100,9 +111,10 @@ export default { ...@@ -100,9 +111,10 @@ export default {
}, },
mounted() { mounted() {
this.setInitialState({ this.setInitialState({
dropdownOnly: this.dropdownOnly, variant: this.variant,
allowLabelEdit: this.allowLabelEdit, allowLabelEdit: this.allowLabelEdit,
allowLabelCreate: this.allowLabelCreate, allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect,
allowScopedLabels: this.allowScopedLabels, allowScopedLabels: this.allowScopedLabels,
selectedLabels: this.selectedLabels, selectedLabels: this.selectedLabels,
labelsFetchPath: this.labelsFetchPath, labelsFetchPath: this.labelsFetchPath,
...@@ -148,13 +160,20 @@ export default { ...@@ -148,13 +160,20 @@ export default {
// as the dropdown wrapper is not using `GlDropdown` as // as the dropdown wrapper is not using `GlDropdown` as
// it will also require us to use `BDropdownForm` // it will also require us to use `BDropdownForm`
// which is yet to be implemented in GitLab UI. // which is yet to be implemented in GitLab UI.
const hasExceptionClass = [
'js-dropdown-button',
'js-btn-cancel-create',
'js-sidebar-dropdown-toggle',
].some(className => target?.classList.contains(className));
const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
className => $(target).parents(className).length,
);
if ( if (
this.showDropdownButton &&
this.showDropdownContents && this.showDropdownContents &&
!$(target).parents('.js-btn-back').length && !hadExceptionParent &&
!$(target).parents('.js-labels-list').length && !hasExceptionClass &&
!target?.classList.contains('js-btn-cancel-create') &&
!target?.classList.contains('js-sidebar-dropdown-toggle') &&
!this.$refs.dropdownButtonCollapsed?.$el.contains(target) && !this.$refs.dropdownButtonCollapsed?.$el.contains(target) &&
!this.$refs.dropdownContents?.$el.contains(target) !this.$refs.dropdownContents?.$el.contains(target)
) { ) {
...@@ -175,10 +194,12 @@ export default { ...@@ -175,10 +194,12 @@ export default {
</script> </script>
<template> <template>
<div class="labels-select-wrapper position-relative"> <div
<div v-if="!dropdownOnly"> class="labels-select-wrapper position-relative"
:class="{ 'is-standalone': isDropdownVariantStandalone }"
>
<template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="allowLabelCreate"
ref="dropdownButtonCollapsed" ref="dropdownButtonCollapsed"
:labels="selectedLabels" :labels="selectedLabels"
@onValueClick="handleCollapsedValueClick" @onValueClick="handleCollapsedValueClick"
...@@ -190,8 +211,18 @@ export default { ...@@ -190,8 +211,18 @@ export default {
<dropdown-value v-show="!showDropdownButton"> <dropdown-value v-show="!showDropdownButton">
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<dropdown-button v-show="showDropdownButton" /> <dropdown-button v-show="dropdownButtonVisible" />
<dropdown-contents v-if="showDropdownButton && showDropdownContents" ref="dropdownContents" /> <dropdown-contents
</div> v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
/>
</template>
<template v-if="isDropdownVariantStandalone">
<dropdown-button v-show="dropdownButtonVisible" />
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
/>
</template>
</div> </div>
</template> </template>
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { DropdownVariant } from '../constants';
/** /**
* Returns string representing current labels * Returns string representing current labels
...@@ -6,8 +7,11 @@ import { __, s__, sprintf } from '~/locale'; ...@@ -6,8 +7,11 @@ import { __, s__, sprintf } from '~/locale';
* *
* @param {object} state * @param {object} state
*/ */
export const dropdownButtonText = state => { export const dropdownButtonText = (state, getters) => {
const selectedLabels = state.labels.filter(label => label.set); const selectedLabels = getters.isDropdownVariantSidebar
? state.labels.filter(label => label.set)
: state.selectedLabels;
if (!selectedLabels.length) { if (!selectedLabels.length) {
return __('Label'); return __('Label');
} else if (selectedLabels.length > 1) { } else if (selectedLabels.length > 1) {
...@@ -26,5 +30,19 @@ export const dropdownButtonText = state => { ...@@ -26,5 +30,19 @@ export const dropdownButtonText = state => {
*/ */
export const selectedLabelsList = state => state.selectedLabels.map(label => label.id); export const selectedLabelsList = state => state.selectedLabels.map(label => label.id);
/**
* 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';
import { DropdownVariant } from '../constants';
export default { export default {
[types.SET_INITIAL_STATE](state, props) { [types.SET_INITIAL_STATE](state, props) {
...@@ -10,7 +11,7 @@ export default { ...@@ -10,7 +11,7 @@ export default {
}, },
[types.TOGGLE_DROPDOWN_CONTENTS](state) { [types.TOGGLE_DROPDOWN_CONTENTS](state) {
if (!state.dropdownOnly) { if (state.variant === DropdownVariant.Sidebar) {
state.showDropdownButton = !state.showDropdownButton; state.showDropdownButton = !state.showDropdownButton;
} }
state.showDropdownContents = !state.showDropdownContents; state.showDropdownContents = !state.showDropdownContents;
...@@ -68,7 +69,16 @@ export default { ...@@ -68,7 +69,16 @@ export default {
set: !label.set, set: !label.set,
}); });
} else { } else {
allLabels.push(label); // In case multiselect is not allowed
// we unselect any existing selected label
const unchangedLabel = state.allowMultiselect
? label
: {
...label,
touched: true,
set: false,
};
allLabels.push(unchangedLabel);
} }
return allLabels; return allLabels;
}, []); }, []);
......
...@@ -13,10 +13,11 @@ export default () => ({ ...@@ -13,10 +13,11 @@ export default () => ({
labelsFilterBasePath: '', labelsFilterBasePath: '',
// UI Flags // UI Flags
variant: '',
allowLabelCreate: false, allowLabelCreate: false,
allowLabelEdit: false, allowLabelEdit: false,
allowScopedLabels: false, allowScopedLabels: false,
dropdownOnly: false, allowMultiselect: false,
showDropdownButton: false, showDropdownButton: false,
showDropdownContents: false, showDropdownContents: false,
showDropdownContentsCreateView: false, showDropdownContentsCreateView: false,
......
...@@ -1032,6 +1032,16 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { ...@@ -1032,6 +1032,16 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
} }
.labels-select-wrapper { .labels-select-wrapper {
&.is-standalone {
.labels-select-dropdown-contents {
max-height: 350px;
.dropdown-content {
height: 250px;
}
}
}
.labels-select-dropdown-contents { .labels-select-dropdown-contents {
min-height: $dropdown-min-height; min-height: $dropdown-min-height;
max-height: 330px; max-height: 330px;
......
...@@ -67,7 +67,7 @@ export default { ...@@ -67,7 +67,7 @@ export default {
// sidebar size. // sidebar size.
debounce(() => { debounce(() => {
this.sidebarExpandedOnClick = true; this.sidebarExpandedOnClick = true;
if (contentContainer) { if (this.canUpdate && contentContainer) {
contentContainer contentContainer
.querySelector('.js-sidebar-dropdown-toggle') .querySelector('.js-sidebar-dropdown-toggle')
.dispatchEvent(new Event('click', { bubbles: true, cancelable: false })); .dispatchEvent(new Event('click', { bubbles: true, cancelable: false }));
...@@ -111,12 +111,14 @@ export default { ...@@ -111,12 +111,14 @@ export default {
<labels-select-vue <labels-select-vue
:allow-label-edit="canUpdate" :allow-label-edit="canUpdate"
:allow-label-create="true" :allow-label-create="true"
:allow-multiselect="true"
:allow-scoped-labels="scopedLabels" :allow-scoped-labels="scopedLabels"
:selected-labels="labels" :selected-labels="labels"
:labels-select-in-progress="epicLabelsSelectInProgress" :labels-select-in-progress="epicLabelsSelectInProgress"
:labels-fetch-path="labelsPath" :labels-fetch-path="labelsPath"
:labels-manage-path="labelsWebUrl" :labels-manage-path="labelsWebUrl"
:labels-filter-base-path="epicsWebUrl" :labels-filter-base-path="epicsWebUrl"
variant="sidebar"
class="block labels js-labels-block" class="block labels js-labels-block"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
@onDropdownClose="handleDropdownClose" @onDropdownClose="handleDropdownClose"
......
...@@ -175,7 +175,7 @@ describe 'Epic show', :js do ...@@ -175,7 +175,7 @@ describe 'Epic show', :js do
it 'shows label create view when `Create group label` is clicked' do it 'shows label create view when `Create group label` is clicked' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('button', text: 'Create group label').click find('a', text: 'Create group label').click
expect(page).to have_selector('.js-labels-create') expect(page).to have_selector('.js-labels-create')
end end
...@@ -183,7 +183,7 @@ describe 'Epic show', :js do ...@@ -183,7 +183,7 @@ describe 'Epic show', :js do
it 'creates new label using create view' do it 'creates new label using create view' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('button', text: 'Create group label').click find('a', text: 'Create group label').click
find('.dropdown-input .gl-form-input').set('Test label') find('.dropdown-input .gl-form-input').set('Test label')
find('.suggest-colors-dropdown a', match: :first).click find('.suggest-colors-dropdown a', match: :first).click
...@@ -200,7 +200,7 @@ describe 'Epic show', :js do ...@@ -200,7 +200,7 @@ describe 'Epic show', :js do
it 'shows labels list view when `Cancel` button is clicked from create view' do it 'shows labels list view when `Cancel` button is clicked from create view' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('button', text: 'Create group label').click find('a', text: 'Create group label').click
find('.js-btn-cancel-create').click find('.js-btn-cancel-create').click
wait_for_requests wait_for_requests
...@@ -211,7 +211,7 @@ describe 'Epic show', :js do ...@@ -211,7 +211,7 @@ describe 'Epic show', :js do
it 'shows labels list view when back button is clicked from create view' do it 'shows labels list view when back button is clicked from create view' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('button', text: 'Create group label').click find('a', text: 'Create group label').click
find('.js-btn-back').click find('.js-btn-back').click
wait_for_requests wait_for_requests
......
...@@ -33,9 +33,32 @@ describe('DropdownButton', () => { ...@@ -33,9 +33,32 @@ describe('DropdownButton', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('methods', () => {
describe('handleButtonClick', () => {
it('calls action `toggleDropdownContents` and stops event propagation when `state.variant` is "standalone"', () => {
const event = {
stopPropagation: jest.fn(),
};
wrapper = createComponent({
...mockConfig,
variant: 'standalone',
});
jest.spyOn(wrapper.vm, 'toggleDropdownContents');
wrapper.vm.handleButtonClick(event);
expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
wrapper.destroy();
});
});
});
describe('template', () => { describe('template', () => {
it('renders component container element', () => { it('renders component container element', () => {
expect(wrapper.is('gl-deprecated-button-stub')).toBe(true); expect(wrapper.is('gl-button-stub')).toBe(true);
}); });
it('renders button text element', () => { it('renders button text element', () => {
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDeprecatedButton, GlIcon, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store'; import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
...@@ -127,12 +127,12 @@ describe('DropdownContentsCreateView', () => { ...@@ -127,12 +127,12 @@ describe('DropdownContentsCreateView', () => {
it('renders dropdown back button element', () => { it('renders dropdown back button element', () => {
const backBtnEl = wrapper const backBtnEl = wrapper
.find('.dropdown-title') .find('.dropdown-title')
.findAll(GlDeprecatedButton) .findAll(GlButton)
.at(0); .at(0);
expect(backBtnEl.exists()).toBe(true); expect(backBtnEl.exists()).toBe(true);
expect(backBtnEl.attributes('aria-label')).toBe('Go back'); expect(backBtnEl.attributes('aria-label')).toBe('Go back');
expect(backBtnEl.find(GlIcon).props('name')).toBe('arrow-left'); expect(backBtnEl.props('icon')).toBe('arrow-left');
}); });
it('renders dropdown title element', () => { it('renders dropdown title element', () => {
...@@ -145,12 +145,12 @@ describe('DropdownContentsCreateView', () => { ...@@ -145,12 +145,12 @@ describe('DropdownContentsCreateView', () => {
it('renders dropdown close button element', () => { it('renders dropdown close button element', () => {
const closeBtnEl = wrapper const closeBtnEl = wrapper
.find('.dropdown-title') .find('.dropdown-title')
.findAll(GlDeprecatedButton) .findAll(GlButton)
.at(1); .at(1);
expect(closeBtnEl.exists()).toBe(true); expect(closeBtnEl.exists()).toBe(true);
expect(closeBtnEl.attributes('aria-label')).toBe('Close'); expect(closeBtnEl.attributes('aria-label')).toBe('Close');
expect(closeBtnEl.find(GlIcon).props('name')).toBe('close'); expect(closeBtnEl.props('icon')).toBe('close');
}); });
it('renders label title input element', () => { it('renders label title input element', () => {
...@@ -192,7 +192,7 @@ describe('DropdownContentsCreateView', () => { ...@@ -192,7 +192,7 @@ describe('DropdownContentsCreateView', () => {
it('renders create button element', () => { it('renders create button element', () => {
const createBtnEl = wrapper const createBtnEl = wrapper
.find('.dropdown-actions') .find('.dropdown-actions')
.findAll(GlDeprecatedButton) .findAll(GlButton)
.at(0); .at(0);
expect(createBtnEl.exists()).toBe(true); expect(createBtnEl.exists()).toBe(true);
...@@ -213,7 +213,7 @@ describe('DropdownContentsCreateView', () => { ...@@ -213,7 +213,7 @@ describe('DropdownContentsCreateView', () => {
it('renders cancel button element', () => { it('renders cancel button element', () => {
const cancelBtnEl = wrapper const cancelBtnEl = wrapper
.find('.dropdown-actions') .find('.dropdown-actions')
.findAll(GlDeprecatedButton) .findAll(GlButton)
.at(1); .at(1);
expect(cancelBtnEl.exists()).toBe(true); expect(cancelBtnEl.exists()).toBe(true);
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDeprecatedButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
...@@ -41,13 +41,19 @@ const createComponent = (initialState = mockConfig) => { ...@@ -41,13 +41,19 @@ const createComponent = (initialState = mockConfig) => {
describe('DropdownContentsLabelsView', () => { describe('DropdownContentsLabelsView', () => {
let wrapper; let wrapper;
let wrapperStandalone;
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
wrapperStandalone = createComponent({
...mockConfig,
variant: 'standalone',
});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapperStandalone.destroy();
}); });
describe('computed', () => { describe('computed', () => {
...@@ -165,13 +171,24 @@ describe('DropdownContentsLabelsView', () => { ...@@ -165,13 +171,24 @@ describe('DropdownContentsLabelsView', () => {
}); });
describe('handleLabelClick', () => { describe('handleLabelClick', () => {
it('calls action `updateSelectedLabels` with provided `label` param', () => { beforeEach(() => {
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation(); jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
});
it('calls action `updateSelectedLabels` with provided `label` param', () => {
wrapper.vm.handleLabelClick(mockRegularLabel); wrapper.vm.handleLabelClick(mockRegularLabel);
expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]); expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]);
}); });
it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => {
jest.spyOn(wrapper.vm, 'toggleDropdownContents');
wrapper.vm.$store.state.allowMultiselect = false;
wrapper.vm.handleLabelClick(mockRegularLabel);
expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
});
}); });
}); });
...@@ -198,12 +215,15 @@ describe('DropdownContentsLabelsView', () => { ...@@ -198,12 +215,15 @@ describe('DropdownContentsLabelsView', () => {
expect(titleEl.text()).toBe('Assign labels'); expect(titleEl.text()).toBe('Assign labels');
}); });
it('does not render dropdown title element when `state.variant` is "standalone"', () => {
expect(wrapperStandalone.find('.dropdown-title').exists()).toBe(false);
});
it('renders dropdown close button element', () => { it('renders dropdown close button element', () => {
const closeButtonEl = wrapper.find('.dropdown-title').find(GlDeprecatedButton); const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
expect(closeButtonEl.exists()).toBe(true); expect(closeButtonEl.exists()).toBe(true);
expect(closeButtonEl.find(GlIcon).exists()).toBe(true); expect(closeButtonEl.props('icon')).toBe('close');
expect(closeButtonEl.find(GlIcon).props('name')).toBe('close');
}); });
it('renders label search input element', () => { it('renders label search input element', () => {
...@@ -253,13 +273,36 @@ describe('DropdownContentsLabelsView', () => { ...@@ -253,13 +273,36 @@ describe('DropdownContentsLabelsView', () => {
}); });
it('renders footer list items', () => { it('renders footer list items', () => {
const createLabelBtn = wrapper.find('.dropdown-footer').find(GlDeprecatedButton); const createLabelLink = wrapper
const manageLabelsLink = wrapper.find('.dropdown-footer').find(GlLink); .find('.dropdown-footer')
.findAll(GlLink)
expect(createLabelBtn.exists()).toBe(true); .at(0);
expect(createLabelBtn.text()).toBe('Create label'); const manageLabelsLink = wrapper
.find('.dropdown-footer')
.findAll(GlLink)
.at(1);
expect(createLabelLink.exists()).toBe(true);
expect(createLabelLink.text()).toBe('Create label');
expect(manageLabelsLink.exists()).toBe(true); expect(manageLabelsLink.exists()).toBe(true);
expect(manageLabelsLink.text()).toBe('Manage labels'); expect(manageLabelsLink.text()).toBe('Manage labels');
}); });
it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', () => {
wrapper.vm.$store.state.allowLabelCreate = false;
return wrapper.vm.$nextTick(() => {
const createLabelLink = wrapper
.find('.dropdown-footer')
.findAll(GlLink)
.at(0);
expect(createLabelLink.text()).not.toBe('Create label');
});
});
it('does not render footer list items when `state.variant` is "standalone"', () => {
expect(wrapperStandalone.find('.dropdown-footer').exists()).toBe(false);
});
}); });
}); });
...@@ -89,6 +89,19 @@ describe('LabelsSelectRoot', () => { ...@@ -89,6 +89,19 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative'); expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
}); });
it('renders component root element with CSS class `is-standalone` when `state.variant` is "standalone"', () => {
const wrapperStandalone = createComponent({
...mockConfig,
variant: 'standalone',
});
return wrapperStandalone.vm.$nextTick(() => {
expect(wrapperStandalone.classes()).toContain('is-standalone');
wrapperStandalone.destroy();
});
});
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => { it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
}); });
...@@ -101,7 +114,9 @@ describe('LabelsSelectRoot', () => { ...@@ -101,7 +114,9 @@ describe('LabelsSelectRoot', () => {
const wrapperDropdownValue = createComponent(mockConfig, { const wrapperDropdownValue = createComponent(mockConfig, {
default: 'None', default: 'None',
}); });
wrapperDropdownValue.vm.$store.state.showDropdownButton = false;
return wrapperDropdownValue.vm.$nextTick(() => {
const valueComp = wrapperDropdownValue.find(DropdownValue); const valueComp = wrapperDropdownValue.find(DropdownValue);
expect(valueComp.exists()).toBe(true); expect(valueComp.exists()).toBe(true);
...@@ -109,6 +124,7 @@ describe('LabelsSelectRoot', () => { ...@@ -109,6 +124,7 @@ describe('LabelsSelectRoot', () => {
wrapperDropdownValue.destroy(); wrapperDropdownValue.destroy();
}); });
});
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => { it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => {
wrapper.vm.$store.dispatch('toggleDropdownButton'); wrapper.vm.$store.dispatch('toggleDropdownButton');
......
...@@ -30,8 +30,10 @@ export const mockConfig = { ...@@ -30,8 +30,10 @@ export const mockConfig = {
allowLabelEdit: true, allowLabelEdit: true,
allowLabelCreate: true, allowLabelCreate: true,
allowScopedLabels: true, allowScopedLabels: true,
allowMultiselect: true,
labelsListTitle: 'Assign labels', labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label', labelsCreateTitle: 'Create label',
variant: 'sidebar',
dropdownOnly: false, dropdownOnly: false,
selectedLabels: [mockRegularLabel, mockScopedLabel], selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false, labelsSelectInProgress: false,
......
...@@ -5,19 +5,25 @@ describe('LabelsSelect Getters', () => { ...@@ -5,19 +5,25 @@ describe('LabelsSelect Getters', () => {
it('returns string "Label" when state.labels has no selected labels', () => { it('returns string "Label" when state.labels has no selected labels', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
expect(getters.dropdownButtonText({ labels })).toBe('Label'); expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
'Label',
);
}); });
it('returns label title when state.labels has only 1 label', () => { it('returns label title when state.labels has only 1 label', () => {
const labels = [{ id: 1, title: 'Foobar', set: true }]; const labels = [{ id: 1, title: 'Foobar', set: true }];
expect(getters.dropdownButtonText({ labels })).toBe('Foobar'); expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
'Foobar',
);
}); });
it('returns first label title and remaining labels count when state.labels has more than 1 label', () => { it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
const labels = [{ id: 1, title: 'Foo', set: true }, { id: 2, title: 'Bar', set: true }]; const labels = [{ id: 1, title: 'Foo', set: true }, { id: 2, title: 'Bar', set: true }];
expect(getters.dropdownButtonText({ labels })).toBe('Foo +1 more'); expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
'Foo +1 more',
);
}); });
}); });
...@@ -28,4 +34,16 @@ describe('LabelsSelect Getters', () => { ...@@ -28,4 +34,16 @@ describe('LabelsSelect Getters', () => {
expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]); expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]);
}); });
}); });
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);
});
});
}); });
...@@ -29,6 +29,7 @@ describe('LabelsSelect Mutations', () => { ...@@ -29,6 +29,7 @@ describe('LabelsSelect Mutations', () => {
const state = { const state = {
dropdownOnly: false, dropdownOnly: false,
showDropdownButton: false, showDropdownButton: false,
variant: 'sidebar',
}; };
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state); mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
...@@ -154,10 +155,27 @@ describe('LabelsSelect Mutations', () => { ...@@ -154,10 +155,27 @@ describe('LabelsSelect Mutations', () => {
describe(`${types.UPDATE_SELECTED_LABELS}`, () => { describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => { it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `true`', () => {
const updatedLabelIds = [2, 4]; const updatedLabelIds = [2, 4];
const state = { const state = {
labels, labels,
allowMultiselect: true,
};
mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
state.labels.forEach(label => {
if (updatedLabelIds.includes(label.id)) {
expect(label.touched).toBe(true);
expect(label.set).toBe(true);
}
});
});
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `false`', () => {
const updatedLabelIds = [2];
const state = {
labels,
allowMultiselect: false,
}; };
mutations[types.UPDATE_SELECTED_LABELS](state, { labels }); mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
......
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