Commit d0d366ca authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '292104-epic-select-migration' into 'master'

Feat(EpicSelect): Migrate epic select

See merge request gitlab-org/gitlab!52528
parents 6b7dc1fa 73056f3c
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
import {
GlLink,
GlLoadingIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { noneEpic } from 'ee/vue_shared/constants';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import createStore from './store';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import { DropdownVariant, DATA_REFETCH_DELAY } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownContents from './dropdown_contents.vue';
import { DropdownVariant } from './constants';
export const i18n = {
selectEpic: s__('Epics|Select epic'),
searchEpic: s__('Epics|Search epics'),
assignEpic: s__('Epics|Assign Epic'),
noMatch: __('No Matching Results'),
};
export default {
i18n,
noneEpic,
store: createStore(),
components: {
GlLink,
GlLoadingIcon,
DropdownTitle,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
DropdownValue,
DropdownValueCollapsed,
DropdownButton,
DropdownHeader,
DropdownSearchInput,
DropdownContents,
},
props: {
groupId: {
......@@ -51,11 +56,6 @@ export default {
type: Boolean,
required: true,
},
blockTitle: {
type: String,
required: false,
default: __('Epic'),
},
initialEpic: {
type: Object,
required: true,
......@@ -72,12 +72,13 @@ export default {
showHeader: {
type: Boolean,
required: false,
default: false,
default: true,
},
},
data() {
return {
showDropdown: this.variant === DropdownVariant.Standalone,
isDropdownShowing: false,
search: '',
};
},
computed: {
......@@ -89,11 +90,37 @@ export default {
'selectedEpicIssueId',
]),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone', 'groupEpics']),
dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress;
},
dropdownButtonTextClass() {
return { 'is-default': this.isDropdownVariantStandalone };
return {
'is-default': this.isDropdownVariantStandalone,
'dropdown-menu-toggle js-epic-select js-extra-options gl-py-3!': true,
};
},
dropDownTitle() {
return this.selectedEpic.title || this.$options.i18n.selectEpic;
},
dropdownClass() {
if (this.isDropdownVariantSidebar) {
return this.isDropdownShowing ? 'dropdown-menu-epics' : 'gl-display-none';
}
return 'dropdown-menu-epics';
},
dropdownHeaderText() {
if (this.showHeader) {
return this.$options.i18n.assignEpic;
}
return '';
},
isLoading() {
return this.epicsFetchInProgress || this.epicSelectInProgress || this.initialEpicLoading;
},
epicListValid() {
return this.groupEpics.length > 0 && !this.isLoading;
},
epicListNotValid() {
return this.groupEpics.length === 0 && !this.isLoading;
},
},
watch: {
......@@ -133,6 +160,9 @@ export default {
this.fetchEpics();
}
},
search: debounce(function debouncedEpicSearch() {
this.setSearchQuery(this.search);
}, DATA_REFETCH_DELAY),
},
mounted() {
this.setInitialData({
......@@ -142,8 +172,6 @@ export default {
selectedEpic: this.initialEpic,
selectedEpicIssueId: this.epicIssueId,
});
$(this.$refs.dropdown).on('shown.bs.dropdown', () => this.fetchEpics());
$(this.$refs.dropdown).on('hidden.bs.dropdown', this.handleDropdownHidden);
},
methods: {
...mapActions([
......@@ -156,26 +184,12 @@ export default {
'assignIssueToEpic',
'removeIssueFromEpic',
]),
handleEditClick() {
this.showDropdown = true;
// Wait for component to render dropdown container
this.$nextTick(() => {
// We're not calling $.dropdown('show') to open
// dropdown and instead triggerring click on button
// so that clicking outside can make dropdown close
// additionally, this approach requires event trigger
// to be deferred so that it doesn't close
setTimeout(() => {
$(this.$refs.dropdownButton.$el).trigger('click');
});
});
},
handleDropdownHidden() {
this.showDropdown = this.isDropdownVariantStandalone;
},
handleItemSelect(epic) {
if (this.selectedEpicIssueId && epic.id === noneEpic.id && epic.title === noneEpic.title) {
if (
this.selectedEpicIssueId &&
epic.id === this.$options.noneEpic.id &&
epic.title === this.$options.noneEpic.title
) {
this.removeIssueFromEpic(this.selectedEpic);
} else if (this.issueId) {
this.assignIssueToEpic(epic);
......@@ -183,50 +197,95 @@ export default {
this.$emit('epicSelect', epic);
}
},
hideDropdown() {
this.isDropdownShowing = this.isDropdownVariantStandalone;
},
toggleFormDropdown() {
const { dropdown } = this.$refs.dropdown.$refs;
this.isDropdownShowing = !this.isDropdownShowing;
if (dropdown && this.isDropdownShowing) {
dropdown.show();
this.fetchEpics();
}
},
},
};
</script>
<template>
<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-if="isDropdownVariantSidebar" v-show="!showDropdown" :epic="selectedEpic">
<slot></slot>
</dropdown-value>
<div
v-if="canEdit || isDropdownVariantStandalone"
v-show="showDropdown"
class="epic-dropdown-container"
>
<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 || showHeader" />
<dropdown-search-input @onSearchInput="setSearchQuery" />
<dropdown-contents
v-if="!epicsFetchInProgress"
:epics="groupEpics"
:selected-epic="selectedEpic"
@onItemSelect="handleItemSelect"
/>
<gl-loading-icon
v-if="epicsFetchInProgress"
class="dropdown-contents-loading"
size="md"
/>
</div>
</div>
<div class="hide-collapsed epic-dropdown-container">
<p
v-if="isDropdownVariantSidebar"
class="title gl-display-flex gl-justify-content-space-between"
>
<span>
{{ __('Epic')
}}<gl-loading-icon v-if="epicSelectInProgress" class="gl-ml-2" :inline="true"
/></span>
<gl-link
v-if="canEdit"
ref="editButton"
class="sidebar-dropdown-toggle"
href="#"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
{{ __('Edit') }}
</gl-link>
</p>
<gl-dropdown
v-if="canEdit || isDropdownVariantStandalone"
ref="dropdown"
:text="dropDownTitle"
class="gl-w-full"
:class="dropdownClass"
:toggle-class="dropdownButtonTextClass"
:header-text="dropdownHeaderText"
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
@toggle="toggleFormDropdown"
>
<template #header>
<gl-search-box-by-type v-model.trim="search" :placeholder="$options.i18n.searchEpic" />
</template>
<template v-if="epicListValid">
<gl-dropdown-item
:active="!selectedEpic"
active-class="is-active"
:is-check-item="true"
:is-checked="selectedEpic.id === $options.noneEpic.id"
@click="handleItemSelect($options.noneEpic)"
>
{{ __('No Epic') }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="epic in groupEpics"
:key="epic.id"
:active="selectedEpic.id === epic.id"
active-class="is-active"
:is-check-item="true"
:is-checked="selectedEpic.id === epic.id"
@click="handleItemSelect(epic)"
>{{ epic.title }}</gl-dropdown-item
>
</template>
<p v-else-if="epicListNotValid" class="gl-mx-5 gl-my-4">
{{ $options.i18n.noMatch }}
</p>
<gl-loading-icon v-else />
</gl-dropdown>
</div>
<div v-if="isDropdownVariantSidebar && !isDropdownShowing">
<dropdown-value-collapsed :epic="selectedEpic" />
<dropdown-value :epic="selectedEpic">
<slot></slot>
</dropdown-value>
</div>
</div>
</template>
......@@ -2,3 +2,5 @@ export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
};
export const DATA_REFETCH_DELAY = 250;
<script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
selectedEpicTitle: {
type: String,
required: false,
default: '',
},
toggleTextClass: {
type: Object,
required: false,
default: null,
},
},
computed: {
buttonText() {
return this.selectedEpicTitle || __('Epic');
},
},
};
</script>
<template>
<button
type="button"
class="dropdown-menu-toggle js-epic-select js-extra-options gl-w-full"
data-display="static"
data-toggle="dropdown"
>
<span class="dropdown-toggle-text" :class="toggleTextClass">{{ buttonText }}</span>
<gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
</button>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import { noneEpic } from 'ee/vue_shared/constants';
export default {
noneEpic,
components: {
GlLink,
},
props: {
epics: {
type: Array,
required: true,
},
selectedEpic: {
type: Object,
required: false,
default: () => null,
},
},
computed: {
isNoEpic() {
return (
this.selectedEpic.id === this.$options.noneEpic.id &&
this.selectedEpic.title === this.$options.noneEpic.title
);
},
},
methods: {
isSelected(epic) {
return this.selectedEpic.id === epic.id;
},
handleItemClick(epic) {
if (epic.id !== this.selectedEpic.id) {
this.$emit('onItemSelect', epic);
} else if (epic.id !== noneEpic.id) {
this.$emit('onItemSelect', noneEpic);
}
},
},
};
</script>
<template>
<div class="dropdown-content">
<ul>
<li data-epic-id="None">
<gl-link
:class="{ 'is-active': isNoEpic }"
@click.prevent="handleItemClick($options.noneEpic)"
>{{ __('No Epic') }}</gl-link
>
</li>
<li class="divider"></li>
<template v-if="epics.length">
<li v-for="epic in epics" :key="epic.id">
<gl-link
:class="{ 'is-active': isSelected(epic) }"
@click.prevent="handleItemClick(epic)"
>{{ epic.title }}</gl-link
>
</li>
</template>
<li v-else class="d-block text-center p-2">{{ __('No matches found') }}</li>
</ul>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
export default {
components: {
GlButton,
},
};
</script>
<template>
<div class="dropdown-title gl-display-flex gl-align-items-center">
<span class="gl-ml-auto">{{ __('Assign epic') }}</span>
<gl-button
:aria-label="__('Close')"
category="tertiary"
class="dropdown-title-button dropdown-menu-close gl-ml-auto"
size="small"
icon="close"
/>
</div>
</template>
<script>
import { debounce } from 'lodash';
import { GlButton, GlIcon } from '@gitlab/ui';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
export default {
components: {
GlButton,
GlIcon,
},
directives: {
autofocusonshow,
},
data() {
return {
query: '',
};
},
methods: {
handleKeyUp: debounce(function debouncedKeyUp() {
this.$emit('onSearchInput', this.query);
}, 300),
handleInputClear() {
this.query = '';
this.handleKeyUp();
},
},
};
</script>
<template>
<div :class="{ 'has-value': query }" class="dropdown-input">
<input
v-model.trim="query"
v-autofocusonshow
:placeholder="__('Search')"
autocomplete="off"
class="dropdown-input-field"
type="search"
@keyup="handleKeyUp"
/>
<gl-icon v-show="!query" class="dropdown-input-search" name="search" />
<gl-button
variant="link"
icon="close"
class="dropdown-input-clear js-dropdown-input-clear"
data-hidden="true"
@click.stop="handleInputClear"
/>
</div>
</template>
<script>
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
GlLink,
},
props: {
canEdit: {
type: Boolean,
required: true,
},
blockTitle: {
type: String,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="title hide-collapsed align-items-center gl-mb-3">
<div class="flex-grow-1">
<span :class="{ 'align-text-top': isLoading }">{{ blockTitle }}</span>
<gl-loading-icon v-show="isLoading" inline />
</div>
<template v-if="canEdit">
<gl-link
class="edit-link float-right sidebar-dropdown-toggle"
@click="$emit('onClickEdit', $event)"
>{{ __('Edit') }}</gl-link
>
</template>
</div>
</template>
......@@ -10,4 +10,4 @@
.col-md-10.col-lg-8
.issuable-form-select-holder
%input{ id: 'issue_epic_id', type: 'hidden', name: 'issue[epic_id]' }
#js-epic-select-root{ data: { group_id: project.group.id } }
#js-epic-select-root{ data: { group_id: project.group.id, show_header: 'true' } }
---
title: 'Feat(EpicSelect): Migrate epic select to internal GitLab UI compoenent'
merge_request: 52528
author:
type: changed
......@@ -31,7 +31,7 @@ RSpec.describe 'Issue Boards', :js do
project.add_maintainer(user)
project.team.add_developer(user2)
gitlab_sign_in(user)
sign_in user
visit project_board_path(project, board)
wait_for_requests
......@@ -152,6 +152,7 @@ RSpec.describe 'Issue Boards', :js do
before do
stub_licensed_features(epics: true)
group.add_owner(user)
visit project_board_path(project, board)
wait_for_requests
end
......@@ -185,7 +186,7 @@ RSpec.describe 'Issue Boards', :js do
page.find('.sidebar-dropdown-toggle').click
wait_for_requests
click_link epic2.title
find('.gl-new-dropdown-item', text: epic2.title).click
wait_for_requests
expect(page.find('.value')).to have_content(epic2.title)
......
......@@ -93,7 +93,7 @@ RSpec.describe 'Issues > Epic bulk assignment', :js do
page.within('.issues-bulk-update') do
click_button 'Select epic'
items.map do |item|
find('.gl-link', text: item).click
find('.gl-new-dropdown-item', text: item).click
end
end
end
......
......@@ -16,6 +16,7 @@ RSpec.describe 'Epic in issue sidebar', :js do
context 'projects within a group' do
before do
group.add_owner(user)
sign_in user
visit project_issue_path(project, issue)
end
......@@ -34,7 +35,7 @@ RSpec.describe 'Epic in issue sidebar', :js do
wait_for_requests
expect(page).to have_selector('.js-epic-select', visible: true)
expect(page.all('.dropdown-content li a').length).to eq(4) # `No Epic` + 3 epics
expect(page.all('.gl-new-dropdown-contents .gl-new-dropdown-item').length).to eq(4) # `No Epic` + 3 epics
end
end
......@@ -44,11 +45,11 @@ RSpec.describe 'Epic in issue sidebar', :js do
wait_for_requests
page.find('.dropdown-input-field').send_keys('Foo')
page.find('.gl-form-input').send_keys('Foo')
wait_for_requests
expect(page).to have_selector('.dropdown-content li a', count: 2) # `No Epic` + 1 matching epic
expect(page).to have_selector('.gl-new-dropdown-contents .gl-new-dropdown-item', count: 2) # `No Epic` + 1 matching epic
end
end
......@@ -58,7 +59,7 @@ RSpec.describe 'Epic in issue sidebar', :js do
wait_for_requests
click_link epic2.title
find('.gl-new-dropdown-item', text: epic2.title).click
wait_for_requests
......
......@@ -47,7 +47,7 @@ RSpec.describe "User creates issue", :js do
it 'creates an issue with no epic' do
click_button 'Select epic'
click_link('No Epic')
find('.gl-new-dropdown-item', text: 'No Epic').click
click_button('Submit issue')
wait_for_all_requests
......@@ -61,7 +61,7 @@ RSpec.describe "User creates issue", :js do
it 'credates an issue with an epic' do
click_button 'Select epic'
click_link(epic.title)
find('.gl-new-dropdown-item', text: epic.title).click
click_button('Submit issue')
wait_for_all_requests
......
......@@ -97,7 +97,6 @@ describe('SidebarItemEpicsSelect', () => {
describe('template', () => {
it('should render epics-select component', () => {
expect(wrapper.find(EpicsSelect).element).toBe(wrapper.element);
expect(wrapper.attributes('blocktitle')).toBe('Epic');
expect(wrapper.text()).toBe('None');
});
});
......
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownButton from 'ee/vue_shared/components/sidebar/epics_select/dropdown_button.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', () => {
it('should render button element', () => {
expect(wrapper.element.tagName).toBe('BUTTON');
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['dropdown-menu-toggle', 'js-epic-select', 'js-extra-options']),
);
expect(wrapper.attributes('data-display')).toBe('static');
expect(wrapper.attributes('data-toggle')).toBe('dropdown');
});
it('should render button title', () => {
const titleEl = wrapper.find('.dropdown-toggle-text');
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', () => {
const iconEl = wrapper.find(GlIcon);
expect(iconEl.exists()).toBe(true);
expect(iconEl.attributes('name')).toBe('chevron-down');
});
});
});
});
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mockEpic1, mockEpic2, mockEpics, noneEpic } from '../mock_data';
const epics = mockEpics.map((epic) => convertObjectPropsToCamelCase(epic));
describe('EpicsSelect', () => {
describe('DropdownContents', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(DropdownContents, {
propsData: {
epics,
selectedEpic: mockEpic1,
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('isNoEpic', () => {
it('should return true when `selectedEpic` is of type `No Epic`', (done) => {
wrapper.setProps({
selectedEpic: noneEpic,
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.isNoEpic).toBe(true);
done();
});
});
it('should return false when `selectedEpic` is an epic', () => {
expect(wrapper.vm.isNoEpic).toBe(false);
});
});
});
describe('methods', () => {
describe('isSelected', () => {
it('should return true when passed `epic` param ID is same as `selectedEpic` prop', () => {
expect(wrapper.vm.isSelected(mockEpic1)).toBe(true);
});
it('should return false when passed `epic` param ID is different from `selectedEpic` prop', () => {
expect(wrapper.vm.isSelected(mockEpic2)).toBe(false);
});
});
describe('handleItemClick', () => {
it('should emit `onItemSelect` event with `epic` param when passed `epic` param is different from already selected epic', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.handleItemClick(mockEpic2);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onItemSelect', mockEpic2);
});
it('should emit `onItemSelect` event with `No Epic` param when passed `epic` param is same as already selected epic', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.handleItemClick(mockEpic1);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onItemSelect', noneEpic);
});
});
});
describe('template', () => {
it('should render container element', () => {
expect(wrapper.classes()).toContain('dropdown-content');
});
it('should render `No Epic` as first item within list', () => {
const noneEl = wrapper.find('ul > li');
expect(noneEl.attributes('data-epic-id')).toBe('None');
expect(noneEl.find(GlLink).exists()).toBe(true);
expect(noneEl.find(GlLink).text()).toBe('No Epic');
});
it('should render epics list for all provided epics', () => {
const epicsEl = wrapper.findAll('ul > li');
expect(epicsEl).toHaveLength(epics.length + 2); // includes divider & No Epic` <li>.
expect(epicsEl.at(1).classes()).toContain('divider');
expect(epicsEl.at(2).find(GlLink).text()).toBe(epics[0].title);
expect(epicsEl.at(3).find(GlLink).text()).toBe(epics[1].title);
expect(epicsEl.at(2).find(GlLink).classes()).toContain('is-active');
});
it('should render string "No matches found" when `epics` array is empty', () => {
wrapper.setProps({
epics: [],
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.text()).toContain('No matches found');
});
});
});
});
});
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownHeader from 'ee/vue_shared/components/sidebar/epics_select/dropdown_header.vue';
describe('EpicsSelect', () => {
describe('DropdownHeader', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(DropdownHeader);
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('should render container element', () => {
expect(wrapper.classes()).toContain('dropdown-title');
});
it('should render title', () => {
expect(wrapper.find('span').text()).toBe('Assign epic');
});
it('should render close button', () => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.attributes('aria-label')).toBe('Close');
expect(buttonEl.classes()).toEqual(
expect.arrayContaining(['dropdown-title-button', 'dropdown-menu-close']),
);
expect(buttonEl.props('icon')).toBe('close');
});
});
});
});
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownSearchInput from 'ee/vue_shared/components/sidebar/epics_select/dropdown_search_input.vue';
const createComponent = () =>
shallowMount(DropdownSearchInput, {
directives: {
/**
* We don't want any observers
* initialized during tests that this
* directive does.
*/
autofocusonshow: {},
},
});
describe('EpicsSelect', () => {
describe('DropdownSearchInput', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('methods', () => {
describe('handleKeyUp', () => {
it('should emit `onSearchInput` on component with `query` param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.setData({
query: 'foo',
});
wrapper.vm.handleKeyUp();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onSearchInput', 'foo');
});
});
describe('handleInputClear', () => {
it('should set `query` prop to empty string and calls `handleKeyUp`', () => {
jest.spyOn(wrapper.vm, 'handleKeyUp');
wrapper.setData({
query: 'foo',
});
wrapper.vm.handleInputClear();
expect(wrapper.vm.query).toBe('');
expect(wrapper.vm.handleKeyUp).toHaveBeenCalled();
});
});
});
describe('template', () => {
it('should render component container', () => {
expect(wrapper.classes()).toContain('dropdown-input');
expect(wrapper.classes()).not.toContain('has-value');
});
it('should add `has-value` class to container when `query` prop is not empty', () => {
wrapper.setData({
query: 'foo',
});
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.classes()).toContain('has-value');
});
});
it('should render input element', () => {
const inputEl = wrapper.find('input');
expect(inputEl.exists()).toBe(true);
expect(inputEl.classes()).toContain('dropdown-input-field');
expect(inputEl.attributes('placeholder')).toBe('Search');
expect(inputEl.attributes('type')).toBe('search');
expect(inputEl.attributes('autocomplete')).toBe('off');
});
it('should render Icon component', () => {
wrapper.setData({
query: 'foo',
});
return wrapper.vm.$nextTick().then(() => {
const iconEl = wrapper.find(GlIcon);
expect(iconEl.exists()).toBe(true);
expect(iconEl.attributes('name')).toBe('search');
});
});
it('should render input clear button', () => {
const clearButtonEl = wrapper.find(GlButton);
expect(clearButtonEl.exists()).toBe(true);
expect(clearButtonEl.classes()).toEqual(
expect.arrayContaining(['dropdown-input-clear', 'js-dropdown-input-clear']),
);
});
});
});
});
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownTitle from 'ee/vue_shared/components/sidebar/epics_select/dropdown_title.vue';
describe('EpicsSelect', () => {
describe('DropdownTitle', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(DropdownTitle, {
propsData: {
canEdit: false,
blockTitle: 'Epic',
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('should render component container', () => {
expect(wrapper.classes()).toEqual(expect.arrayContaining(['title', 'hide-collapsed']));
});
it('should render title element', () => {
wrapper.setProps({
isLoading: true,
});
return wrapper.vm.$nextTick().then(() => {
const titleEl = wrapper.find('.flex-grow-1');
expect(titleEl.exists()).toBe(true);
expect(titleEl.find('span').classes()).toContain('align-text-top');
expect(titleEl.find('span').text()).toBe('Epic');
});
});
it('should render loading icon when `isLoading` prop is true', () => {
wrapper.setProps({
isLoading: true,
});
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
it('should render edit link when `canEdit` prop is true', () => {
wrapper.setProps({
canEdit: true,
});
return wrapper.vm.$nextTick().then(() => {
const editEl = wrapper.find(GlLink);
expect(editEl.exists()).toBe(true);
expect(editEl.classes()).toContain('sidebar-dropdown-toggle');
expect(editEl.text()).toBe('Edit');
});
});
});
});
});
......@@ -3927,9 +3927,6 @@ msgstr ""
msgid "Assign custom color like #FF0000"
msgstr ""
msgid "Assign epic"
msgstr ""
msgid "Assign labels"
msgstr ""
......@@ -11354,6 +11351,9 @@ msgstr ""
msgid "Epics|Are you sure you want to remove %{bStart}%{targetIssueTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}?"
msgstr ""
msgid "Epics|Assign Epic"
msgstr ""
msgid "Epics|Enter a title for your epic"
msgstr ""
......@@ -11369,6 +11369,12 @@ msgstr ""
msgid "Epics|Remove issue"
msgstr ""
msgid "Epics|Search epics"
msgstr ""
msgid "Epics|Select epic"
msgstr ""
msgid "Epics|Show more"
msgstr ""
......
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