Commit 0927cfc6 authored by Denys Mishunov's avatar Denys Mishunov

Merge branch 'kp-improve-vue-labels-dropdown-performance' into 'master'

Use SmartVirtualList to improve labels list render performance

See merge request gitlab-org/gitlab!32419
parents 427aca0b 19232801
// eslint-disable-next-line import/prefer-default-export
export const DropdownVariant = { export const DropdownVariant = {
Sidebar: 'sidebar', Sidebar: 'sidebar',
Standalone: 'standalone', Standalone: 'standalone',
}; };
export const LIST_BUFFER_SIZE = 5;
...@@ -3,15 +3,20 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -3,15 +3,20 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, 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 SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import LabelItem from './label_item.vue'; import LabelItem from './label_item.vue';
import { LIST_BUFFER_SIZE } from './constants';
export default { export default {
LIST_BUFFER_SIZE,
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
GlSearchBoxByType, GlSearchBoxByType,
GlLink, GlLink,
SmartVirtualList,
LabelItem, LabelItem,
}, },
data() { data() {
...@@ -139,10 +144,18 @@ export default { ...@@ -139,10 +144,18 @@ export default {
<gl-search-box-by-type v-model="searchKey" :autofocus="true" /> <gl-search-box-by-type v-model="searchKey" :autofocus="true" />
</div> </div>
<div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content"> <div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
<ul class="list-unstyled mb-0"> <smart-virtual-list
:length="visibleLabels.length"
:remain="$options.LIST_BUFFER_SIZE"
:size="$options.LIST_BUFFER_SIZE"
wclass="list-unstyled mb-0"
wtag="ul"
class="h-100"
>
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left"> <li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
<label-item <label-item
:label="label" :label="label"
:is-label-set="label.set"
:highlight="index === currentHighlightItem" :highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)" @clickLabel="handleLabelClick(label)"
/> />
...@@ -150,7 +163,7 @@ export default { ...@@ -150,7 +163,7 @@ export default {
<li v-show="!visibleLabels.length" class="p-2 text-center"> <li v-show="!visibleLabels.length" class="p-2 text-center">
{{ __('No matching results') }} {{ __('No matching results') }}
</li> </li>
</ul> </smart-virtual-list>
</div> </div>
<div v-if="isDropdownVariantSidebar" class="dropdown-footer"> <div v-if="isDropdownVariantSidebar" class="dropdown-footer">
<ul class="list-unstyled"> <ul class="list-unstyled">
...@@ -162,9 +175,9 @@ export default { ...@@ -162,9 +175,9 @@ export default {
> >
</li> </li>
<li> <li>
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{ <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
footerManageLabelTitle {{ footerManageLabelTitle }}
}}</gl-link> </gl-link>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -11,6 +11,10 @@ export default { ...@@ -11,6 +11,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isLabelSet: {
type: Boolean,
required: true,
},
highlight: { highlight: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -19,7 +23,7 @@ export default { ...@@ -19,7 +23,7 @@ export default {
}, },
data() { data() {
return { return {
isSet: this.label.set, isSet: this.isLabelSet,
}; };
}, },
computed: { computed: {
...@@ -29,6 +33,16 @@ export default { ...@@ -29,6 +33,16 @@ export default {
}; };
}, },
}, },
watch: {
/**
* This watcher assures that if user used
* `Enter` key to set/unset label, changes
* are reflected here too.
*/
isLabelSet(value) {
this.isSet = value;
},
},
methods: { methods: {
handleClick() { handleClick() {
this.isSet = !this.isSet; this.isSet = !this.isSet;
......
...@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, 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 SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
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';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
...@@ -224,6 +225,10 @@ describe('DropdownContentsLabelsView', () => { ...@@ -224,6 +225,10 @@ describe('DropdownContentsLabelsView', () => {
expect(searchInputEl.attributes('autofocus')).toBe('true'); expect(searchInputEl.attributes('autofocus')).toBe('true');
}); });
it('renders smart-virtual-list element', () => {
expect(wrapper.find(SmartVirtualList).exists()).toBe(true);
});
it('renders label elements for all labels', () => { it('renders label elements for all labels', () => {
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length); expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
}); });
......
...@@ -4,10 +4,13 @@ import { GlIcon, GlLink } from '@gitlab/ui'; ...@@ -4,10 +4,13 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
import { mockRegularLabel } from './mock_data'; import { mockRegularLabel } from './mock_data';
const createComponent = ({ label = mockRegularLabel, highlight = true } = {}) => const mockLabel = { ...mockRegularLabel, set: true };
const createComponent = ({ label = mockLabel, highlight = true } = {}) =>
shallowMount(LabelItem, { shallowMount(LabelItem, {
propsData: { propsData: {
label, label,
isLabelSet: label.set,
highlight, highlight,
}, },
}); });
...@@ -28,13 +31,29 @@ describe('LabelItem', () => { ...@@ -28,13 +31,29 @@ describe('LabelItem', () => {
it('returns an object containing `backgroundColor` based on `label` prop', () => { it('returns an object containing `backgroundColor` based on `label` prop', () => {
expect(wrapper.vm.labelBoxStyle).toEqual( expect(wrapper.vm.labelBoxStyle).toEqual(
expect.objectContaining({ expect.objectContaining({
backgroundColor: mockRegularLabel.color, backgroundColor: mockLabel.color,
}), }),
); );
}); });
}); });
}); });
describe('watchers', () => {
describe('isLabelSet', () => {
it('sets value of `isLabelSet` to `isSet` data prop', () => {
expect(wrapper.vm.isSet).toBe(true);
wrapper.setProps({
isLabelSet: false,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.isSet).toBe(false);
});
});
});
});
describe('methods', () => { describe('methods', () => {
describe('handleClick', () => { describe('handleClick', () => {
it('sets value of `isSet` data prop to opposite of its current value', () => { it('sets value of `isSet` data prop to opposite of its current value', () => {
...@@ -52,7 +71,7 @@ describe('LabelItem', () => { ...@@ -52,7 +71,7 @@ describe('LabelItem', () => {
wrapper.vm.handleClick(); wrapper.vm.handleClick();
expect(wrapper.emitted('clickLabel')).toBeTruthy(); expect(wrapper.emitted('clickLabel')).toBeTruthy();
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockRegularLabel]); expect(wrapper.emitted('clickLabel')[0]).toEqual([mockLabel]);
}); });
}); });
}); });
...@@ -105,7 +124,7 @@ describe('LabelItem', () => { ...@@ -105,7 +124,7 @@ describe('LabelItem', () => {
}); });
it('renders label title', () => { it('renders label title', () => {
expect(wrapper.text()).toContain(mockRegularLabel.title); expect(wrapper.text()).toContain(mockLabel.title);
}); });
}); });
}); });
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