Commit f2ecfe21 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Added dirty state to dropdown contents

Changes logic for dropdown visibility

Fixed tests for labels view

Abstracted intersection observer appearing

Added test for skipping the query

Added tests for dirty state

Apply 1 suggestion(s) to 1 file(s)
Apply 1 suggestion(s) to 1 file(s)
Fixed updating labels

Fixed labels visibility

Fixed labels root spec

Fixed dropdown contents spec

Removed expandDropdown mock
parent 94aeceba
...@@ -3,7 +3,7 @@ import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
import { isDropdownVariantStandalone } from './utils'; import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
export default { export default {
components: { components: {
...@@ -52,11 +52,17 @@ export default { ...@@ -52,11 +52,17 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isVisible: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
showDropdownContentsCreateView: false, showDropdownContentsCreateView: false,
localSelectedLabels: [...this.selectedLabels], localSelectedLabels: [...this.selectedLabels],
isDirty: false,
}; };
}, },
computed: { computed: {
...@@ -87,6 +93,26 @@ export default { ...@@ -87,6 +93,26 @@ export default {
return isDropdownVariantStandalone(this.variant); return isDropdownVariantStandalone(this.variant);
}, },
}, },
watch: {
localSelectedLabels: {
handler() {
this.isDirty = true;
},
deep: true,
},
isVisible(newVal) {
if (newVal) {
this.$refs.dropdown.show();
this.isDirty = false;
} else {
this.$refs.dropdown.hide();
this.setLabels();
}
},
selectedLabels(newVal) {
this.localSelectedLabels = newVal;
},
},
methods: { methods: {
toggleDropdownContentsCreateView() { toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView; this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
...@@ -98,12 +124,16 @@ export default { ...@@ -98,12 +124,16 @@ export default {
this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate(); this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
} }
}, },
showDropdown() { setLabels() {
this.$refs.dropdown.show(); if (!this.isDirty) {
}, return;
closeDropdown() { }
this.$emit('setLabels', this.localSelectedLabels); this.$emit('setLabels', this.localSelectedLabels);
this.$refs.dropdown.hide(); },
handleDropdownHide() {
if (!isDropdownVariantSidebar(this.variant)) {
this.setLabels();
}
}, },
}, },
}; };
...@@ -115,7 +145,7 @@ export default { ...@@ -115,7 +145,7 @@ export default {
:text="buttonText" :text="buttonText"
class="gl-w-full gl-mt-2" class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
@hide="$emit('setLabels', localSelectedLabels)" @hide="handleDropdownHide"
> >
<template #header> <template #header>
<div <div
...@@ -141,7 +171,7 @@ export default { ...@@ -141,7 +171,7 @@ export default {
class="dropdown-header-button gl-p-0!" class="dropdown-header-button gl-p-0!"
icon="close" icon="close"
data-testid="close-button" data-testid="close-button"
@click="closeDropdown" @click="$emit('closeDropdown')"
/> />
</div> </div>
</template> </template>
......
...@@ -50,6 +50,7 @@ export default { ...@@ -50,6 +50,7 @@ export default {
return { return {
searchKey: '', searchKey: '',
labels: [], labels: [],
isVisible: false,
}; };
}, },
apollo: { apollo: {
...@@ -64,13 +65,13 @@ export default { ...@@ -64,13 +65,13 @@ export default {
}; };
}, },
skip() { skip() {
return this.searchKey.length === 1; return this.searchKey.length === 1 || !this.isVisible;
}, },
update: (data) => data.workspace?.labels?.nodes || [], update: (data) => data.workspace?.labels?.nodes || [],
async result() { async result() {
if (this.$refs.searchInput) { if (this.$refs.searchInput) {
await this.$nextTick; await this.$nextTick;
this.focusInputField(); this.$refs.searchInput.focusInput();
} }
}, },
error() { error() {
...@@ -150,7 +151,8 @@ export default { ...@@ -150,7 +151,8 @@ export default {
setSearchKey(value) { setSearchKey(value) {
this.searchKey = value; this.searchKey = value;
}, },
focusInputField() { onDropdownAppear() {
this.isVisible = true;
this.$refs.searchInput.focusInput(); this.$refs.searchInput.focusInput();
}, },
}, },
...@@ -158,7 +160,7 @@ export default { ...@@ -158,7 +160,7 @@ export default {
</script> </script>
<template> <template>
<gl-intersection-observer @appear="focusInputField"> <gl-intersection-observer @appear="onDropdownAppear">
<gl-dropdown-form class="labels-select-contents-list js-labels-list"> <gl-dropdown-form class="labels-select-contents-list js-labels-list">
<gl-search-box-by-type <gl-search-box-by-type
ref="searchInput" ref="searchInput"
......
...@@ -136,14 +136,14 @@ export default { ...@@ -136,14 +136,14 @@ export default {
methods: { methods: {
handleDropdownClose(labels) { handleDropdownClose(labels) {
this.$emit('updateSelectedLabels', labels); this.$emit('updateSelectedLabels', labels);
this.collapseEditableItem();
},
collapseEditableItem() {
this.$refs.editable?.collapse(); this.$refs.editable?.collapse();
}, },
handleCollapsedValueClick() { handleCollapsedValueClick() {
this.$emit('toggleCollapse'); this.$emit('toggleCollapse');
}, },
showDropdownContents() {
this.$refs.dropdownContents.showDropdown();
},
isDropdownVariantSidebar, isDropdownVariantSidebar,
isDropdownVariantStandalone, isDropdownVariantStandalone,
isDropdownVariantEmbedded, isDropdownVariantEmbedded,
...@@ -170,7 +170,6 @@ export default { ...@@ -170,7 +170,6 @@ export default {
:title="__('Labels')" :title="__('Labels')"
:loading="isLoading" :loading="isLoading"
:can-edit="allowLabelEdit" :can-edit="allowLabelEdit"
@open="showDropdownContents"
> >
<template #collapsed> <template #collapsed>
<dropdown-value <dropdown-value
...@@ -184,7 +183,7 @@ export default { ...@@ -184,7 +183,7 @@ export default {
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
</template> </template>
<template #default> <template #default="{ edit }">
<dropdown-value <dropdown-value
:disable-labels="labelsSelectInProgress" :disable-labels="labelsSelectInProgress"
:selected-labels="issuableLabels" :selected-labels="issuableLabels"
...@@ -197,7 +196,6 @@ export default { ...@@ -197,7 +196,6 @@ export default {
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<dropdown-contents <dropdown-contents
ref="dropdownContents"
:dropdown-button-text="dropdownButtonText" :dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle" :labels-list-title="labelsListTitle"
...@@ -207,7 +205,9 @@ export default { ...@@ -207,7 +205,9 @@ export default {
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:variant="variant" :variant="variant"
:issuable-type="issuableType" :issuable-type="issuableType"
:is-visible="edit"
@setLabels="handleDropdownClose" @setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/> />
</template> </template>
</sidebar-editable-item> </sidebar-editable-item>
......
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import {
GlLoadingIcon,
GlSearchBoxByType,
GlDropdownItem,
GlIntersectionObserver,
} from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
...@@ -68,23 +73,31 @@ describe('DropdownContentsLabelsView', () => { ...@@ -68,23 +73,31 @@ describe('DropdownContentsLabelsView', () => {
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem); const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findLabelsList = () => wrapper.find('[data-testid="labels-list"]'); const findLabelsList = () => wrapper.find('[data-testid="labels-list"]');
const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]'); const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]');
async function makeObserverAppear() {
await findObserver().vm.$emit('appear');
}
describe('when loading labels', () => { describe('when loading labels', () => {
it('renders disabled search input field', async () => { it('renders disabled search input field', async () => {
createComponent(); createComponent();
await makeObserverAppear();
expect(findSearchInput().props('disabled')).toBe(true); expect(findSearchInput().props('disabled')).toBe(true);
}); });
it('renders loading icon', async () => { it('renders loading icon', async () => {
createComponent(); createComponent();
await makeObserverAppear();
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
}); });
it('does not render labels list', async () => { it('does not render labels list', async () => {
createComponent(); createComponent();
await makeObserverAppear();
expect(findLabelsList().exists()).toBe(false); expect(findLabelsList().exists()).toBe(false);
}); });
}); });
...@@ -92,6 +105,7 @@ describe('DropdownContentsLabelsView', () => { ...@@ -92,6 +105,7 @@ describe('DropdownContentsLabelsView', () => {
describe('when labels are loaded', () => { describe('when labels are loaded', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent(); createComponent();
await makeObserverAppear();
await waitForPromises(); await waitForPromises();
}); });
...@@ -121,6 +135,7 @@ describe('DropdownContentsLabelsView', () => { ...@@ -121,6 +135,7 @@ describe('DropdownContentsLabelsView', () => {
}, },
}), }),
}); });
await makeObserverAppear();
findSearchInput().vm.$emit('input', '123'); findSearchInput().vm.$emit('input', '123');
await waitForPromises(); await waitForPromises();
await nextTick(); await nextTick();
...@@ -130,6 +145,7 @@ describe('DropdownContentsLabelsView', () => { ...@@ -130,6 +145,7 @@ describe('DropdownContentsLabelsView', () => {
it('calls `createFlash` when fetching labels failed', async () => { it('calls `createFlash` when fetching labels failed', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') }); createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') });
await makeObserverAppear();
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises(); await waitForPromises();
...@@ -138,9 +154,17 @@ describe('DropdownContentsLabelsView', () => { ...@@ -138,9 +154,17 @@ describe('DropdownContentsLabelsView', () => {
it('emits an `input` event on label click', async () => { it('emits an `input` event on label click', async () => {
createComponent(); createComponent();
await makeObserverAppear();
await waitForPromises(); await waitForPromises();
findFirstLabel().trigger('click'); findFirstLabel().trigger('click');
expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels)); expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels));
}); });
it('does not trigger query when component did not appear', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(false);
expect(findLabelsList().exists()).toBe(false);
expect(successfulQueryHandler).not.toHaveBeenCalled();
});
}); });
...@@ -19,6 +19,7 @@ const GlDropdownStub = { ...@@ -19,6 +19,7 @@ const GlDropdownStub = {
`, `,
methods: { methods: {
show: showDropdown, show: showDropdown,
hide: jest.fn(),
}, },
}; };
...@@ -68,18 +69,52 @@ describe('DropdownContent', () => { ...@@ -68,18 +69,52 @@ describe('DropdownContent', () => {
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]'); const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
it('emits `show` for dropdown on call showDropdown', () => { it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent(); createComponent();
wrapper.vm.showDropdown(); await wrapper.setProps({
isVisible: true,
});
expect(findDropdown().emitted('show')).toBeUndefined(); expect(findDropdown().emitted('show')).toBeUndefined();
}); });
it('emits `setLabels` event on dropdown hide', () => { it('does not emit `setLabels` event on dropdown hide if labels did not change', () => {
createComponent(); createComponent();
findDropdown().vm.$emit('hide'); findDropdown().vm.$emit('hide');
expect(wrapper.emitted('setLabels')).toEqual([[mockLabels]]); expect(wrapper.emitted('setLabels')).toBeUndefined();
});
it('emits `setLabels` event on dropdown hide if labels changed on non-sidebar widget', async () => {
createComponent({ props: { variant: DropdownVariant.Standalone } });
const updatedLabel = {
id: 28,
title: 'Bug',
description: 'Label for bugs',
color: '#FF0000',
textColor: '#FFFFFF',
};
findLabelsView().vm.$emit('input', [updatedLabel]);
await nextTick();
findDropdown().vm.$emit('hide');
expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]);
});
it('emits `setLabels` event on visibility change if labels changed on sidebar widget', async () => {
createComponent({ props: { variant: DropdownVariant.Standalone, isVisible: true } });
const updatedLabel = {
id: 28,
title: 'Bug',
description: 'Label for bugs',
color: '#FF0000',
textColor: '#FFFFFF',
};
findLabelsView().vm.$emit('input', [updatedLabel]);
wrapper.setProps({ isVisible: false });
await nextTick();
expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]);
}); });
it('does not render header on standalone variant', () => { it('does not render header on standalone variant', () => {
......
...@@ -27,8 +27,6 @@ describe('LabelsSelectRoot', () => { ...@@ -27,8 +27,6 @@ describe('LabelsSelectRoot', () => {
const findDropdownValue = () => wrapper.findComponent(DropdownValue); const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const findDropdownContents = () => wrapper.findComponent(DropdownContents); const findDropdownContents = () => wrapper.findComponent(DropdownContents);
const expandDropdown = () => wrapper.vm.$refs.editable.expand();
const createComponent = ({ const createComponent = ({
config = mockConfig, config = mockConfig,
slots = {}, slots = {},
...@@ -125,23 +123,11 @@ describe('LabelsSelectRoot', () => { ...@@ -125,23 +123,11 @@ describe('LabelsSelectRoot', () => {
}); });
}); });
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if there are labels to update', async () => { it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' }; const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent(); createComponent();
wrapper.vm.$refs.dropdownContents = {
showDropdown: jest.fn(),
};
const showSpy = jest.spyOn(wrapper.vm.$refs.dropdownContents, 'showDropdown');
findDropdownContents().vm.$refs.dropdown = {
show: jest.fn(),
};
await waitForPromises();
expandDropdown();
await nextTick();
findDropdownContents().vm.$emit('setLabels', [label]); findDropdownContents().vm.$emit('setLabels', [label]);
expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]); expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
expect(showSpy).toHaveBeenCalled();
}); });
}); });
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