Commit 2a18d518 authored by Kushal Pandya's avatar Kushal Pandya

Refactor LabelToken to use BaseToken

Refactors LabelToken to use BaseToken and enable
values caching for Vue Epics List and Roadmap.
parent 9e6c4d65
......@@ -21,7 +21,7 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
]);
export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }]; // eslint-disable-line @gitlab/require-i18n-strings
export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
{ value: 'Upcoming', text: __('Upcoming') }, // eslint-disable-line @gitlab/require-i18n-strings
......
......@@ -120,7 +120,7 @@ export default {
}, DEBOUNCE_DELAY);
},
handleTokenValueSelected(activeTokenValue) {
if (this.isRecentTokenValuesEnabled) {
if (this.isRecentTokenValuesEnabled && activeTokenValue) {
setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
}
},
......
<script>
import {
GlToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
import { DEFAULT_LABELS } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
export default {
components: {
BaseToken,
GlToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
},
props: {
config: {
......@@ -32,43 +25,24 @@ export default {
type: Object,
required: true,
},
active: {
type: Boolean,
required: true,
},
},
data() {
return {
labels: this.config.initialLabels || [],
defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: true,
loading: false,
};
},
computed: {
currentValue() {
return this.value.data.toLowerCase();
},
activeLabel() {
return this.labels.find(
(label) => this.getLabelName(label).toLowerCase() === stripQuotes(this.currentValue),
methods: {
getActiveLabel(labels, currentValue) {
return labels.find(
(label) => this.getLabelName(label).toLowerCase() === stripQuotes(currentValue),
);
},
containerStyle() {
if (this.activeLabel) {
const { color, textColor } = convertObjectPropsToCamelCase(this.activeLabel);
return { backgroundColor: color, color: textColor };
}
return {};
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.labels.length) {
this.fetchLabelBySearchTerm(this.value.data);
}
},
},
},
methods: {
/**
* There's an inconsistency between private and public API
* for labels where label name is included in a different
......@@ -84,6 +58,16 @@ export default {
getLabelName(label) {
return label.name || label.title;
},
getContainerStyle(activeLabel) {
if (activeLabel) {
const { color: backgroundColor, textColor: color } = convertObjectPropsToCamelCase(
activeLabel,
);
return { backgroundColor, color };
}
return {};
},
fetchLabelBySearchTerm(searchTerm) {
this.loading = true;
this.config
......@@ -99,50 +83,47 @@ export default {
this.loading = false;
});
},
searchLabels: debounce(function debouncedSearch({ data }) {
if (!this.loading) this.fetchLabelBySearchTerm(data);
}, DEBOUNCE_DELAY),
},
};
</script>
<template>
<gl-filtered-search-token
:config="config"
v-bind="{ ...$props, ...$attrs }"
v-on="$listeners"
@input="searchLabels"
<base-token
:token-config="config"
:token-value="value"
:token-active="active"
:tokens-list-loading="loading"
:token-values="labels"
:fn-active-token-value="getActiveLabel"
:default-token-values="defaultLabels"
:recent-token-values-storage-key="config.recentTokenValuesStorageKey"
@fetch-token-values="fetchLabelBySearchTerm"
>
<template #view-token="{ inputValue, cssClasses, listeners }">
<gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners"
>~{{ activeLabel ? getLabelName(activeLabel) : inputValue }}</gl-token
<template
#view-token="{ viewTokenProps: { inputValue, cssClasses, listeners, activeTokenValue } }"
>
<gl-token
variant="search-value"
:class="cssClasses"
:style="getContainerStyle(activeTokenValue)"
v-on="listeners"
>~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token
>
</template>
<template #suggestions>
<template #token-values-list="{ tokenValues }">
<gl-filtered-search-suggestion
v-for="label in defaultLabels"
:key="label.value"
:value="label.value"
v-for="label in tokenValues"
:key="label.id"
:value="getLabelName(label)"
>
{{ label.text }}
<div class="gl-display-flex gl-align-items-center">
<span
:style="{ backgroundColor: label.color }"
class="gl-display-inline-block mr-2 p-2"
></span>
<div>{{ getLabelName(label) }}</div>
</div>
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultLabels.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
v-for="label in labels"
:key="label.id"
:value="getLabelName(label)"
>
<div class="gl-display-flex gl-align-items-center">
<span
:style="{ backgroundColor: label.color }"
class="gl-display-inline-block mr-2 p-2"
></span>
<div>{{ getLabelName(label) }}</div>
</div>
</gl-filtered-search-suggestion>
</template>
</template>
</gl-filtered-search-token>
</base-token>
</template>
......@@ -30,7 +30,8 @@ export default {
],
token: LabelToken,
unique: false,
symbol: '~',
// eslint-disable-next-line @gitlab/require-i18n-strings
defaultLabels: [{ value: 'No label', text: __('No label') }],
fetchLabels: this.fetchLabels,
},
{
......
......@@ -62,6 +62,7 @@ export default {
symbol: '~',
token: LabelToken,
operators: OPERATOR_IS_ONLY,
recentTokenValuesStorageKey: `${this.groupFullPath}-epics-recent-tokens-label_name`,
fetchLabels: (search = '') => {
const params = {
only_group_labels: true,
......
......@@ -41,7 +41,7 @@ describe('EpicFilteredSearch', () => {
],
token: LabelToken,
unique: false,
symbol: '~',
defaultLabels: [{ value: 'No label', text: 'No label' }],
fetchLabels: wrapper.vm.fetchLabels,
},
{
......
......@@ -185,6 +185,7 @@ describe('RoadmapFilters', () => {
symbol: '~',
token: LabelToken,
operators,
recentTokenValuesStorageKey: 'gitlab-org-epics-recent-tokens-label_name',
fetchLabels: expect.any(Function),
},
{
......
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
......@@ -18,6 +17,7 @@ import {
DEFAULT_LABELS,
DEFAULT_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data';
......@@ -25,6 +25,7 @@ import { mockLabelToken } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
Portal: true,
BaseToken,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
......@@ -68,55 +69,17 @@ describe('LabelToken', () => {
wrapper.destroy();
});
describe('computed', () => {
beforeEach(async () => {
// Label title with spaces is always enclosed in quotations by component.
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
wrapper.setData({
labels: mockLabels,
});
await wrapper.vm.$nextTick();
});
describe('currentValue', () => {
it('returns lowercase string for `value.data`', () => {
expect(wrapper.vm.currentValue).toBe('"foo label"');
});
});
describe('activeLabel', () => {
it('returns object for currently present `value.data`', () => {
expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel);
});
});
describe('containerStyle', () => {
it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => {
expect(wrapper.vm.containerStyle).toEqual({
backgroundColor: mockRegularLabel.color,
color: mockRegularLabel.textColor,
});
});
it('returns empty object when `activeLabel` is not set', async () => {
wrapper.setData({
labels: [],
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.containerStyle).toEqual({});
});
});
});
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('getActiveLabel', () => {
it('returns label object from labels array based on provided `currentValue` param', () => {
expect(wrapper.vm.getActiveLabel(mockLabels, 'foo label')).toEqual(mockRegularLabel);
});
});
describe('getLabelName', () => {
it('returns value of `name` or `title` property present in provided label param', () => {
let mockLabel = {
......@@ -187,8 +150,14 @@ describe('LabelToken', () => {
await wrapper.vm.$nextTick();
});
it('renders gl-filtered-search-token component', () => {
expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
it('renders base-token component', () => {
const baseTokenEl = wrapper.find(BaseToken);
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
tokenValues: mockLabels,
fnActiveTokenValue: wrapper.vm.getActiveLabel,
});
});
it('renders token item when value is selected', () => {
......
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