Commit 3b9fc200 authored by Martin Wortschack's avatar Martin Wortschack

Add milestone token component

- Use milestone token
in code review analytics
filtered search
parent 911c4841
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import MilestoneToken from '../../shared/components/tokens/milestone_token.vue';
export default { export default {
components: { components: {
...@@ -17,6 +18,7 @@ export default { ...@@ -17,6 +18,7 @@ export default {
milestonePath: 'milestonePath', milestonePath: 'milestonePath',
labelsPath: 'labelsPath', labelsPath: 'labelsPath',
milestones: state => state.milestones.data, milestones: state => state.milestones.data,
milestonesLoading: state => state.milestones.isLoading,
}), }),
tokens() { tokens() {
return [ return [
...@@ -24,9 +26,11 @@ export default { ...@@ -24,9 +26,11 @@ export default {
icon: 'clock', icon: 'clock',
title: __('Milestone'), title: __('Milestone'),
type: 'milestone', type: 'milestone',
token: GlFilteredSearchToken, token: MilestoneToken,
options: this.milestones, milestones: this.milestones,
unique: true, unique: true,
symbol: '%',
isLoading: this.milestonesLoading,
}, },
]; ];
}, },
...@@ -36,22 +40,30 @@ export default { ...@@ -36,22 +40,30 @@ export default {
}, },
methods: { methods: {
...mapActions('filters', ['fetchMilestones', 'setFilters']), ...mapActions('filters', ['fetchMilestones', 'setFilters']),
filteredSearchSubmit(filters) { processFilters(filters) {
const result = filters.reduce((acc, item) => { return filters.reduce((acc, token) => {
const { const { type, value } = token;
type, let tokenValue = value.data;
value: { data },
} = item; // remove wrapping double quotes which were added for token values that include spaces
if (
(tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
) {
tokenValue = tokenValue.slice(1, -1);
}
if (!acc[type]) { if (!acc[type]) {
acc[type] = []; acc[type] = [];
} }
acc[type].push(data); acc[type].push(tokenValue);
return acc; return acc;
}, {}); }, {});
},
this.setFilters({ label_name: result.label, milestone_title: result.milestone }); filteredSearchSubmit(filters) {
const { label, milestone } = this.processFilters(filters);
this.setFilters({ label_name: label, milestone_title: milestone });
}, },
}, },
}; };
......
<script>
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
},
inheritAttrs: false,
props: {
config: {
type: Object,
required: true,
},
value: {
type: Object,
required: true,
},
},
computed: {
milestones() {
return this.config.milestones;
},
filteredMilestones() {
return this.milestones.filter(
milestone => milestone.title.toLowerCase().indexOf(this.value.data?.toLowerCase()) !== -1,
);
},
},
methods: {
getEscapedText(text) {
let escapedText = text;
const hasSpace = text.indexOf(' ') !== -1;
const hasDoubleQuote = text.indexOf('"') !== -1;
// Encapsulate value with quotes if it has spaces
// Known side effect: values's with both single and double quotes
// won't escape properly
if (hasSpace) {
if (hasDoubleQuote) {
escapedText = `'${text}'`;
} else {
// Encapsulate singleQuotes or if it hasSpace
escapedText = `"${text}"`;
}
}
return escapedText;
},
},
defaultSuggestions: [
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'None', text: __('None') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Any', text: __('Any') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Upcoming', text: __('Upcoming') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Started', text: __('Started') },
],
};
</script>
<template>
<gl-filtered-search-token :config="config" v-bind="{ ...this.$attrs }" v-on="$listeners">
<template #view="{ inputValue }">
<template v-if="config.symbol">{{ config.symbol }}</template
>{{ inputValue }}
</template>
<template #suggestions>
<gl-filtered-search-suggestion
v-for="suggestion in $options.defaultSuggestions"
:key="suggestion.value"
:value="suggestion.value"
>{{ suggestion.text }}</gl-filtered-search-suggestion
>
<gl-dropdown-divider v-if="config.isLoading || filteredMilestones.length" />
<gl-loading-icon v-if="config.isLoading" />
<template v-else>
<gl-filtered-search-suggestion
v-for="milestone in filteredMilestones"
ref="milestoneItem"
:key="milestone.id"
:value="getEscapedText(milestone.title)"
>
<div>{{ milestone.title }}</div>
</gl-filtered-search-suggestion>
</template>
</template>
</gl-filtered-search-token>
</template>
...@@ -73,9 +73,9 @@ describe('FilteredSearchBar', () => { ...@@ -73,9 +73,9 @@ describe('FilteredSearchBar', () => {
}); });
it('displays options in the milestone token', () => { it('displays options in the milestone token', () => {
const { options } = getSearchToken(milestoneTokenType); const { milestones: milestoneToken } = getSearchToken(milestoneTokenType);
expect(options).toHaveLength(mockMilestones.length); expect(milestoneToken).toHaveLength(mockMilestones.length);
}); });
}); });
...@@ -99,5 +99,50 @@ describe('FilteredSearchBar', () => { ...@@ -99,5 +99,50 @@ describe('FilteredSearchBar', () => {
undefined, undefined,
); );
}); });
it('removes wrapping double quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('submit', [
{ type: 'milestone', value: { data: '"milestone with spaces"', operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
label_name: undefined,
milestone_title: ['milestone with spaces'],
},
undefined,
);
});
it('removes wrapping single quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('submit', [
{ type: 'milestone', value: { data: "'milestone with spaces'", operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
label_name: undefined,
milestone_title: ['milestone with spaces'],
},
undefined,
);
});
it('does not remove inner double quotes from the data and dispatches setFilters ', () => {
findFilteredSearch().vm.$emit('submit', [
{ type: 'milestone', value: { data: 'milestone "with" spaces', operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
label_name: undefined,
milestone_title: ['milestone "with" spaces'],
},
undefined,
);
});
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import { GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import MilestoneToken from 'ee/analytics/shared/components/tokens/milestone_token.vue';
import mockMilestones from './mock_data';
describe('MilestoneToken', () => {
let wrapper;
let value;
let config;
let stubs;
const createComponent = (props = {}, options) => {
wrapper = shallowMount(MilestoneToken, {
propsData: props,
...options,
});
};
const findFilteredSearchSuggestion = index =>
wrapper.findAll(GlFilteredSearchSuggestion).at(index);
const findAllMilestoneSuggestions = () => wrapper.findAll({ ref: 'milestoneItem' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
value = { data: '' };
config = {
icon: 'clock',
title: 'Milestone',
type: 'milestone',
milestones: mockMilestones,
unique: true,
symbol: '%',
isLoading: false,
};
stubs = {
GlFilteredSearchToken: {
template: `<div><slot name="suggestions"></slot></div>`,
},
};
});
it('renders a loading icon', () => {
config.isLoading = true;
createComponent({ config, value: {} }, { stubs });
expect(findLoadingIcon().exists()).toBe(true);
});
describe('suggestions', () => {
describe('default suggestions', () => {
it.each`
text | dropdownIndex
${'None'} | ${0}
${'Any'} | ${1}
${'Upcoming'} | ${2}
${'Started'} | ${3}
`('renders the "$text" suggestion', ({ text, dropdownIndex }) => {
createComponent({ config, value }, { stubs });
expect(findFilteredSearchSuggestion(dropdownIndex).text()).toEqual(text);
});
});
it("adds wrapping quotes to the suggestion's value when the milestone title has spaces", () => {
createComponent({ config, value }, { stubs });
const milestoneWithSpaces = findAllMilestoneSuggestions().at(0);
expect(milestoneWithSpaces.props('value')).toBe(
'"Sprint - Eligendi et aut pariatur ab rerum vel."',
);
});
describe('when no search term is given', () => {
it('renders two milestone suggestions', () => {
createComponent({ config, value }, { stubs });
expect(findAllMilestoneSuggestions()).toHaveLength(2);
});
});
describe('when the search term "v4" is given', () => {
it('renders one milestone suggestion that matches the search term', () => {
value.data = 'v4';
createComponent({ config, value }, { stubs });
expect(findAllMilestoneSuggestions()).toHaveLength(1);
});
});
});
});
export default [
{
id: 41,
title: 'Sprint - Eligendi et aut pariatur ab rerum vel.',
project_id: 1,
description: 'Accusamus qui sapiente porro et in voluptates.',
due_date: '2020-01-14',
created_at: '2020-01-08T15:47:37.697Z',
updated_at: '2020-01-08T15:47:37.697Z',
state: 'active',
iid: 6,
start_date: '2020-01-08',
group_id: null,
name: 'Sprint - Eligendi et aut pariatur ab rerum vel.',
},
{
id: 5,
title: 'v4.0',
project_id: 1,
description: 'Atque laudantium reiciendis consequatur temporibus qui qui.',
due_date: null,
created_at: '2020-01-18T15:46:07.448Z',
updated_at: '2020-01-18T15:46:07.448Z',
state: 'active',
iid: 5,
start_date: null,
group_id: null,
name: 'v4.0',
},
];
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