Commit c15911af authored by Phil Hughes's avatar Phil Hughes

Merge branch 'kp-use-base-token-for-label-token' into 'master'

Refactor LabelToken to use BaseToken

See merge request gitlab-org/gitlab!61992
parents 7daf57fa 2a18d518
......@@ -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