Commit 54cf5455 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '342970-labels-widget-split-header-footer-fix-search' into 'master'

Labels widget - Fix search input

See merge request gitlab-org/gitlab!72659
parents 2ce161f7 bf3eff86
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { debounce } from 'lodash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownHeader from './dropdown_header.vue';
import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
export default {
components: {
DropdownContentsLabelsView,
DropdownContentsCreateView,
DropdownHeader,
DropdownFooter,
GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
},
inject: ['allowLabelCreate', 'labelsManagePath'],
props: {
labelsCreateTitle: {
type: String,
......@@ -72,6 +77,7 @@ export default {
showDropdownContentsCreateView: false,
localSelectedLabels: [...this.selectedLabels],
isDirty: false,
searchKey: '',
};
},
computed: {
......@@ -122,6 +128,12 @@ export default {
this.localSelectedLabels = newVal;
},
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
this.debouncedSearchKeyUpdate.cancel();
},
methods: {
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
......@@ -144,6 +156,12 @@ export default {
this.setLabels();
}
},
setSearchKey(value) {
this.searchKey = value;
},
setFocus() {
this.$refs.header.focusInput();
},
},
};
</script>
......@@ -155,39 +173,26 @@ export default {
class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
@shown="setFocus"
>
<template #header>
<div
<dropdown-header
v-if="!isStandalone"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid="dropdown-header"
>
<gl-button
v-if="showDropdownContentsCreateView"
:aria-label="__('Go back')"
variant="link"
size="small"
class="js-btn-back dropdown-header-button gl-p-0"
icon="arrow-left"
data-testid="go-back-button"
@click.stop="toggleDropdownContent"
/>
<span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
data-testid="close-button"
@click="$emit('closeDropdown')"
ref="header"
v-model="searchKey"
:labels-create-title="labelsCreateTitle"
:labels-list-title="labelsListTitle"
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
@toggleDropdownContentsCreateView="toggleDropdownContent"
@closeDropdown="$emit('closeDropdown')"
@input="debouncedSearchKeyUpdate"
/>
</div>
</template>
<template #default>
<component
:is="dropdownContentsView"
v-model="localSelectedLabels"
:search-key="searchKey"
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
......@@ -197,18 +202,12 @@ export default {
/>
</template>
<template #footer>
<div v-if="showDropdownFooter" data-testid="dropdown-footer">
<gl-dropdown-item
v-if="allowLabelCreate"
data-testid="create-label-button"
@click.capture.native.stop="toggleDropdownContent"
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
<gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
<dropdown-footer
v-if="showDropdownFooter"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
@toggleDropdownContentsCreateView="toggleDropdownContent"
/>
</template>
</gl-dropdown>
</template>
<script>
import {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIntersectionObserver,
} from '@gitlab/ui';
import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue';
......@@ -20,7 +12,6 @@ export default {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIntersectionObserver,
LabelItem,
},
......@@ -48,10 +39,13 @@ export default {
type: String,
required: true,
},
searchKey: {
type: String,
required: true,
},
},
data() {
return {
searchKey: '',
labels: [],
isVisible: false,
};
......@@ -71,12 +65,6 @@ export default {
return this.searchKey.length === 1 || !this.isVisible;
},
update: (data) => data.workspace?.labels?.nodes || [],
async result() {
if (this.$refs.searchInput) {
await this.$nextTick;
this.$refs.searchInput.focusInput();
}
},
error() {
createFlash({ message: __('Error fetching labels.') });
},
......@@ -101,12 +89,6 @@ export default {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
this.debouncedSearchKeyUpdate.cancel();
},
methods: {
isLabelSelected(label) {
return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
......@@ -153,12 +135,8 @@ export default {
this.$emit('closeDropdown', this.localSelectedLabels);
}
},
setSearchKey(value) {
this.searchKey = value;
},
onDropdownAppear() {
this.isVisible = true;
this.$refs.searchInput.focusInput();
},
},
};
......@@ -167,14 +145,6 @@ export default {
<template>
<gl-intersection-observer @appear="onDropdownAppear">
<gl-dropdown-form class="labels-select-contents-list js-labels-list">
<gl-search-box-by-type
ref="searchInput"
:value="searchKey"
:disabled="labelsFetchInProgress"
data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="debouncedSearchKeyUpdate"
/>
<div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
......
<script>
import { GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDropdownItem,
},
inject: ['allowLabelCreate', 'labelsManagePath'],
props: {
footerCreateLabelTitle: {
type: String,
required: true,
},
footerManageLabelTitle: {
type: String,
required: true,
},
},
};
</script>
<template>
<div data-testid="dropdown-footer">
<gl-dropdown-item
v-if="allowLabelCreate"
data-testid="create-label-button"
@click.capture.native.stop="$emit('toggleDropdownContentsCreateView')"
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
<gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
</template>
<script>
import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
export default {
components: {
GlButton,
GlSearchBoxByType,
},
model: {
prop: 'searchKey',
},
props: {
labelsCreateTitle: {
type: String,
required: true,
},
labelsListTitle: {
type: String,
required: true,
},
showDropdownContentsCreateView: {
type: Boolean,
required: true,
},
labelsFetchInProgress: {
type: Boolean,
required: false,
default: false,
},
searchKey: {
type: String,
required: true,
},
},
computed: {
dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
},
},
methods: {
focusInput() {
this.$refs.searchInput.focusInput();
},
},
};
</script>
<template>
<div data-testid="dropdown-header">
<div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
<gl-button
v-if="showDropdownContentsCreateView"
:aria-label="__('Go back')"
variant="link"
size="small"
class="js-btn-back dropdown-header-button gl-p-0"
icon="arrow-left"
data-testid="go-back-button"
@click.stop="$emit('toggleDropdownContentsCreateView')"
/>
<span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
data-testid="close-button"
@click="$emit('closeDropdown')"
/>
</div>
<gl-search-box-by-type
v-if="!showDropdownContentsCreateView"
ref="searchInput"
:value="searchKey"
:disabled="labelsFetchInProgress"
data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="$emit('input', $event)"
/>
</div>
</template>
......@@ -43,6 +43,7 @@ describe('DropdownContentsLabelsView', () => {
initialState = mockConfig,
queryHandler = successfulQueryHandler,
injected = {},
searchKey = '',
} = {}) => {
const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]);
......@@ -57,6 +58,7 @@ describe('DropdownContentsLabelsView', () => {
...initialState,
localSelectedLabels,
issuableType: IssuableType.Issue,
searchKey,
},
stubs: {
GlSearchBoxByType,
......@@ -68,7 +70,6 @@ describe('DropdownContentsLabelsView', () => {
wrapper.destroy();
});
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
......@@ -81,12 +82,6 @@ describe('DropdownContentsLabelsView', () => {
}
describe('when loading labels', () => {
it('renders disabled search input field', async () => {
createComponent();
await makeObserverAppear();
expect(findSearchInput().props('disabled')).toBe(true);
});
it('renders loading icon', async () => {
createComponent();
await makeObserverAppear();
......@@ -107,10 +102,6 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
});
it('renders enabled search input field', async () => {
expect(findSearchInput().props('disabled')).toBe(false);
});
it('does not render loading icon', async () => {
expect(findLoadingIcon().exists()).toBe(false);
});
......@@ -132,9 +123,9 @@ describe('DropdownContentsLabelsView', () => {
},
},
}),
searchKey: '123',
});
await makeObserverAppear();
findSearchInput().vm.$emit('input', '123');
await waitForPromises();
await nextTick();
......
......@@ -4,6 +4,8 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
......@@ -26,7 +28,7 @@ const GlDropdownStub = {
describe('DropdownContent', () => {
let wrapper;
const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
const createComponent = ({ props = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
......@@ -46,11 +48,6 @@ describe('DropdownContent', () => {
...data,
};
},
provide: {
allowLabelCreate: true,
labelsManagePath: 'foo/bar',
...injected,
},
stubs: {
GlDropdown: GlDropdownStub,
},
......@@ -63,13 +60,10 @@ describe('DropdownContent', () => {
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
const findDropdownFooter = () => wrapper.findComponent(DropdownFooter);
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent();
await wrapper.setProps({
......@@ -136,6 +130,16 @@ describe('DropdownContent', () => {
expect(findDropdownHeader().exists()).toBe(true);
});
it('sets searchKey for labels view on input event from header', async () => {
createComponent();
expect(wrapper.vm.searchKey).toEqual('');
findDropdownHeader().vm.$emit('input', '123');
await nextTick();
expect(findLabelsView().props('searchKey')).toEqual('123');
});
describe('Create view', () => {
beforeEach(() => {
createComponent({ data: { showDropdownContentsCreateView: true } });
......@@ -149,16 +153,8 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(false);
});
it('does not render create label button', () => {
expect(findCreateLabelButton().exists()).toBe(false);
});
it('renders go back button', () => {
expect(findGoBackButton().exists()).toBe(true);
});
it('changes the view to Labels view on back button click', async () => {
findGoBackButton().vm.$emit('click', new MouseEvent('click'));
it('changes the view to Labels view on `toggleDropdownContentsCreateView` event', async () => {
findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView');
await nextTick();
expect(findCreateView().exists()).toBe(false);
......@@ -198,32 +194,5 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(true);
});
it('does not render go back button', () => {
expect(findGoBackButton().exists()).toBe(false);
});
it('does not render create label button if `allowLabelCreate` is false', () => {
createComponent({ injected: { allowLabelCreate: false } });
expect(findCreateLabelButton().exists()).toBe(false);
});
describe('when `allowLabelCreate` is true', () => {
beforeEach(() => {
createComponent();
});
it('renders create label button', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
it('changes the view to Create on create label button click', async () => {
findCreateLabelButton().trigger('click');
await nextTick();
expect(findLabelsView().exists()).toBe(false);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
describe('DropdownFooter', () => {
let wrapper;
const createComponent = ({ props = {}, injected = {} } = {}) => {
wrapper = shallowMount(DropdownFooter, {
propsData: {
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
...props,
},
provide: {
allowLabelCreate: true,
labelsManagePath: 'foo/bar',
...injected,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
describe('Labels view', () => {
beforeEach(() => {
createComponent();
});
it('does not render create label button if `allowLabelCreate` is false', () => {
createComponent({ injected: { allowLabelCreate: false } });
expect(findCreateLabelButton().exists()).toBe(false);
});
describe('when `allowLabelCreate` is true', () => {
beforeEach(() => {
createComponent();
});
it('renders create label button', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
it('emits `toggleDropdownContentsCreateView` event on create label button click', async () => {
findCreateLabelButton().trigger('click');
await nextTick();
expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
});
});
});
});
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
describe('DropdownHeader', () => {
let wrapper;
const createComponent = ({
showDropdownContentsCreateView = false,
labelsFetchInProgress = false,
} = {}) => {
wrapper = extendedWrapper(
shallowMount(DropdownHeader, {
propsData: {
showDropdownContentsCreateView,
labelsFetchInProgress,
labelsCreateTitle: 'Create label',
labelsListTitle: 'Select label',
searchKey: '',
},
stubs: {
GlSearchBoxByType,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findGoBackButton = () => wrapper.findByTestId('go-back-button');
beforeEach(() => {
createComponent();
});
describe('Create view', () => {
beforeEach(() => {
createComponent({ showDropdownContentsCreateView: true });
});
it('renders go back button', () => {
expect(findGoBackButton().exists()).toBe(true);
});
it('does not render search input field', async () => {
expect(findSearchInput().exists()).toBe(false);
});
});
describe('Labels view', () => {
beforeEach(() => {
createComponent();
});
it('does not render go back button', () => {
expect(findGoBackButton().exists()).toBe(false);
});
it.each`
labelsFetchInProgress | disabled
${true} | ${true}
${false} | ${false}
`(
'when labelsFetchInProgress is $labelsFetchInProgress, renders search input with disabled prop to $disabled',
({ labelsFetchInProgress, disabled }) => {
createComponent({ labelsFetchInProgress });
expect(findSearchInput().props('disabled')).toBe(disabled);
},
);
});
});
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