Commit 85fc352f authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '323311-add-current-selected-label-in-filter-tab-on-jira-issues-list' into 'master'

Add current selected label in filter tab on Jira issues list

See merge request gitlab-org/gitlab!65817
parents 336e17b4 9c2bacff
......@@ -37,6 +37,7 @@ export const SortDirection = {
ascending: 'ascending',
};
export const FILTERED_SEARCH_LABELS = 'labels';
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
export const TOKEN_TITLE_AUTHOR = __('Author');
......
......@@ -117,6 +117,29 @@ export default {
!this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]),
);
},
showDefaultSuggestions() {
return this.defaultSuggestions.length;
},
showRecentSuggestions() {
return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
},
showPreloadedSuggestions() {
return this.preloadedSuggestions.length && !this.searchKey;
},
showAvailableSuggestions() {
return this.availableSuggestions.length;
},
showSuggestions() {
// These conditions must match the template under `#suggestions` slot
// See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65817#note_632619411
return (
this.showDefaultSuggestions ||
this.showRecentSuggestions ||
this.showPreloadedSuggestions ||
this.suggestionsLoading ||
this.showAvailableSuggestions
);
},
},
watch: {
active: {
......@@ -168,8 +191,8 @@ export default {
<template #view="viewTokenProps">
<slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
</template>
<template #suggestions>
<template v-if="defaultSuggestions.length">
<template v-if="showSuggestions" #suggestions>
<template v-if="showDefaultSuggestions">
<gl-filtered-search-suggestion
v-for="token in defaultSuggestions"
:key="token.value"
......@@ -179,13 +202,13 @@ export default {
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
</template>
<template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey">
<template v-if="showRecentSuggestions">
<gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
<slot name="suggestions-list" :suggestions="recentSuggestions"></slot>
<gl-dropdown-divider />
</template>
<slot
v-if="preloadedSuggestions.length && !searchKey"
v-if="showPreloadedSuggestions"
name="suggestions-list"
:suggestions="preloadedSuggestions"
></slot>
......
......@@ -10,6 +10,14 @@ import {
AvailableSortOptions,
DEFAULT_PAGE_SIZE,
} from '~/issuable_list/constants';
import {
FILTERED_SEARCH_LABELS,
FILTERED_SEARCH_TERM,
OPERATOR_IS_ONLY,
TOKEN_TITLE_LABEL,
} from '~/vue_shared/components/filtered_search_bar/constants';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { ISSUES_LIST_FETCH_ERROR } from '../constants';
import getJiraIssuesQuery from '../graphql/queries/get_jira_issues.query.graphql';
import JiraIssuesListEmptyState from './jira_issues_list_empty_state.vue';
......@@ -117,16 +125,48 @@ export default {
},
},
methods: {
getFilteredSearchValue() {
getFilteredSearchTokens() {
return [
{
type: 'filtered-search-term',
value: {
data: this.filterParams.search || '',
type: FILTERED_SEARCH_LABELS,
icon: 'labels',
symbol: '~',
title: TOKEN_TITLE_LABEL,
unique: false,
token: LabelToken,
operators: OPERATOR_IS_ONLY,
defaultLabels: [],
fetchLabels: () => {
return Promise.resolve([]);
},
},
];
},
getFilteredSearchValue() {
const { labels, search } = this.filterParams || {};
const filteredSearchValue = [];
if (labels) {
filteredSearchValue.push(
...labels.map((label) => ({
type: FILTERED_SEARCH_LABELS,
value: { data: label },
})),
);
}
if (search) {
filteredSearchValue.push({
type: FILTERED_SEARCH_TERM,
value: {
data: search,
},
});
}
return filteredSearchValue;
},
onJiraIssuesQueryError(error) {
createFlash({
message: error.message,
......@@ -147,11 +187,21 @@ export default {
},
onIssuableListFilter(filters = []) {
const filterParams = {};
const labels = [];
const plainText = [];
filters.forEach((filter) => {
if (filter.type === 'filtered-search-term' && filter.value.data) {
plainText.push(filter.value.data);
if (!filter.value.data) return;
switch (filter.type) {
case FILTERED_SEARCH_LABELS:
labels.push(filter.value.data);
break;
case FILTERED_SEARCH_TERM:
plainText.push(filter.value.data);
break;
default:
break;
}
});
......@@ -159,6 +209,10 @@ export default {
filterParams.search = plainText.join(' ');
}
if (labels.length) {
filterParams.labels = labels;
}
this.filterParams = filterParams;
},
},
......@@ -171,7 +225,7 @@ export default {
:tabs="$options.IssuableListTabs"
:current-tab="currentState"
:search-input-placeholder="s__('Integrations|Search Jira issues')"
:search-tokens="[]"
:search-tokens="getFilteredSearchTokens()"
:sort-options="$options.AvailableSortOptions"
:initial-filter-value="getFilteredSearchValue()"
:initial-sort-by="sortedBy"
......
......@@ -8,14 +8,7 @@ Object {
"enableLabelPermalinks": true,
"hasNextPage": false,
"hasPreviousPage": false,
"initialFilterValue": Array [
Object {
"type": "filtered-search-term",
"value": Object {
"data": "",
},
},
],
"initialFilterValue": Array [],
"initialSortBy": "created_desc",
"isManualOrdering": false,
"issuableSymbol": "#",
......@@ -107,7 +100,24 @@ Object {
"previousPage": 0,
"recentSearchesStorageKey": "jira_issues",
"searchInputPlaceholder": "Search Jira issues",
"searchTokens": Array [],
"searchTokens": Array [
Object {
"defaultLabels": Array [],
"fetchLabels": [Function],
"icon": "labels",
"operators": Array [
Object {
"description": "is",
"value": "=",
},
],
"symbol": "~",
"title": "Label",
"token": "LabelTokenMock",
"type": "labels",
"unique": false,
},
],
"showBulkEditSidebar": false,
"showPaginationControls": true,
"sortOptions": Array [
......
......@@ -23,6 +23,10 @@ jest.mock('~/issuable_list/constants', () => ({
IssuableListTabs: jest.requireActual('~/issuable_list/constants').IssuableListTabs,
AvailableSortOptions: jest.requireActual('~/issuable_list/constants').AvailableSortOptions,
}));
jest.mock(
'~/vue_shared/components/filtered_search_bar/tokens/label_token.vue',
() => 'LabelTokenMock',
);
const resolvedValue = {
headers: {
......@@ -49,7 +53,12 @@ describe('JiraIssuesListRoot', () => {
let wrapper;
let mock;
const mockSearchTerm = 'test issue';
const mockLabel = 'ecosystem';
const findIssuableList = () => wrapper.findComponent(IssuableList);
const createLabelFilterEvent = (data) => ({ type: 'labels', value: { data } });
const createSearchFilterEvent = (data) => ({ type: 'filtered-search-term', value: { data } });
const createComponent = ({
apolloProvider = createMockApolloProvider(),
......@@ -109,12 +118,15 @@ describe('JiraIssuesListRoot', () => {
});
describe('with `initialFilterParams` prop', () => {
const mockSearchTerm = 'foo';
beforeEach(async () => {
jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue);
createComponent({ initialFilterParams: { search: mockSearchTerm } });
createComponent({
initialFilterParams: {
labels: [mockLabel],
search: mockSearchTerm,
},
});
await waitForPromises();
});
......@@ -122,6 +134,7 @@ describe('JiraIssuesListRoot', () => {
const issuableList = findIssuableList();
expect(issuableList.props('initialFilterValue')).toEqual([
{ type: 'labels', value: { data: mockLabel } },
{ type: 'filtered-search-term', value: { data: mockSearchTerm } },
]);
expect(issuableList.props('urlParams').search).toBe(mockSearchTerm);
......@@ -215,32 +228,31 @@ describe('JiraIssuesListRoot', () => {
expect(issuableList.props('initialSortBy')).toBe(mockSortBy);
});
it('filter event sets `filterParams` value and calls fetchIssues', async () => {
const mockFilterTerm = 'foo';
const issuableList = findIssuableList();
it.each`
desc | input | expected
${'with label and search'} | ${[createLabelFilterEvent(mockLabel), createSearchFilterEvent(mockSearchTerm)]} | ${{ labels: [mockLabel], search: mockSearchTerm }}
${'with multiple lables'} | ${[createLabelFilterEvent('label1'), createLabelFilterEvent('label2')]} | ${{ labels: ['label1', 'label2'], search: undefined }}
${'with multiple searches'} | ${[createSearchFilterEvent('foo bar'), createSearchFilterEvent('lorem')]} | ${{ labels: undefined, search: 'foo bar lorem' }}
`(
'$desc, filter event sets "filterParams" value and calls fetchIssues',
async ({ input, expected }) => {
const issuableList = findIssuableList();
issuableList.vm.$emit('filter', [
{
type: 'filtered-search-term',
value: {
data: mockFilterTerm,
},
},
]);
await waitForPromises();
issuableList.vm.$emit('filter', input);
await waitForPromises();
expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, {
params: {
labels: undefined,
page: 1,
per_page: 2,
search: mockFilterTerm,
sort: 'created_desc',
state: 'opened',
with_labels_details: true,
},
});
});
expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, {
params: {
page: 1,
per_page: 2,
sort: 'created_desc',
state: 'opened',
with_labels_details: true,
...expected,
},
});
},
);
});
});
......
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