Commit 9524c9d8 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '232465-mlunoe-migrate-vsa-to-use-generic-filter-bar' into 'master'

Refactor(VSA filter bar): use general filter bar

See merge request gitlab-org/gitlab!39588
parents 496819de b8690726
......@@ -10,7 +10,7 @@ import DateRange from '../../shared/components/daterange.vue';
import StageTable from './stage_table.vue';
import DurationChart from './duration_chart.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue';
import UrlSyncMixin from '../../shared/mixins/url_sync_mixin';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { toYmd } from '../../shared/utils';
import RecentActivityCard from './recent_activity_card.vue';
import TimeMetricsCard from './time_metrics_card.vue';
......@@ -40,8 +40,8 @@ export default {
MetricCard,
FilterBar,
ValueStreamSelect,
UrlSync,
},
mixins: [UrlSyncMixin],
props: {
emptyStateSvgPath: {
type: String,
......@@ -126,15 +126,19 @@ export default {
return this.startDate && this.endDate;
},
query() {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
const selectedLabels = this.selectedLabels?.length ? this.selectedLabels : null;
const selectedAssignees = this.selectedAssignees?.length ? this.selectedAssignees : null;
return {
group_id: !this.hideGroupDropDown ? this.currentGroupPath : null,
'project_ids[]': this.selectedProjectIds,
'project_ids[]': selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
milestone_title: this.selectedMilestone,
author_username: this.selectedAuthor,
'label_name[]': this.selectedLabels,
'assignee_username[]': this.selectedAssignees,
'label_name[]': selectedLabels,
'assignee_username[]': selectedAssignees,
};
},
stageCount() {
......@@ -259,10 +263,6 @@ export default {
@selected="onProjectsSelect"
/>
</div>
<filter-bar
v-if="shouldDisplayFilterBar"
class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 mt-md-0 gl-mr-3 gl-border-none"
/>
<div v-if="shouldDisplayFilters" class="gl-justify-content-end gl-white-space-nowrap">
<date-range
:start-date="startDate"
......@@ -274,6 +274,11 @@ export default {
/>
</div>
</div>
<filter-bar
v-if="shouldDisplayFilterBar"
class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 gl-mr-3 gl-border-none"
:group-path="currentGroupPath"
/>
</div>
</div>
<gl-empty-state
......@@ -356,6 +361,7 @@ export default {
</template>
</stage-table>
</div>
<url-sync :query="query" />
</div>
<duration-chart v-if="shouldDisplayDurationChart" class="mt-3" :stages="activeStages" />
<type-of-work-charts v-if="shouldDisplayTypeOfWorkCharts" :is-loading="isLoadingTypeOfWork" />
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlFilteredSearch } from '@gitlab/ui';
import { __ } from '~/locale';
import MilestoneToken from '../../shared/components/tokens/milestone_token.vue';
import LabelToken from '../../shared/components/tokens/label_token.vue';
import UserToken from '../../shared/components/tokens/user_token.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
export const prepareTokens = ({
milestone = null,
......@@ -27,80 +27,69 @@ export const prepareTokens = ({
export default {
name: 'FilterBar',
components: {
GlFilteredSearch,
FilteredSearchBar,
},
props: {
groupPath: {
type: String,
required: true,
},
data() {
return {
value: [],
};
},
computed: {
...mapState('filters', {
milestones: state => state.milestones.data,
milestonesLoading: state => state.milestones.isLoading,
labels: state => state.labels.data,
labelsLoading: state => state.labels.isLoading,
authors: state => state.authors.data,
authorsLoading: state => state.authors.isLoading,
assignees: state => state.assignees.data,
assigneesLoading: state => state.assignees.isLoading,
initialTokens: state => state.initialTokens,
}),
availableTokens() {
tokens() {
return [
{
icon: 'clock',
title: __('Milestone'),
type: 'milestone',
token: MilestoneToken,
milestones: this.milestones,
initialMilestones: this.milestones,
unique: true,
symbol: '%',
isLoading: this.milestonesLoading,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchData: this.fetchMilestones,
fetchMilestones: this.fetchMilestones,
},
{
icon: 'labels',
title: __('Label'),
type: 'labels',
token: LabelToken,
labels: this.labels,
initialLabels: this.labels,
unique: false,
symbol: '~',
isLoading: this.labelsLoading,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchData: this.fetchLabels,
fetchLabels: this.fetchLabels,
},
{
icon: 'pencil',
title: __('Author'),
type: 'author',
token: UserToken,
users: this.authors,
token: AuthorToken,
initialAuthors: this.authors,
unique: true,
isLoading: this.authorsLoading,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchData: this.fetchAuthors,
fetchAuthors: this.fetchAuthors,
},
{
icon: 'user',
title: __('Assignees'),
type: 'assignees',
token: UserToken,
users: this.assignees,
token: AuthorToken,
initialAuthors: this.assignees,
unique: false,
isLoading: this.assigneesLoading,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchData: this.fetchAssignees,
fetchAuthors: this.fetchAssignees,
},
];
},
},
mounted() {
this.initializeTokens();
},
methods: {
...mapActions('filters', [
'setFilters',
......@@ -109,15 +98,15 @@ export default {
'fetchAuthors',
'fetchAssignees',
]),
initializeTokens() {
initialFilterValue() {
const {
selectedMilestone: milestone = null,
selectedAuthor: author = null,
selectedAssignees: assignees = [],
selectedLabels: labels = [],
} = this.initialTokens;
const preparedTokens = prepareTokens({ milestone, author, assignees, labels });
this.value = preparedTokens;
return prepareTokens({ milestone, author, assignees, labels });
},
processFilters(filters) {
return filters.reduce((acc, token) => {
......@@ -142,7 +131,7 @@ export default {
}, {});
},
filteredSearchSubmit(filters) {
handleFilter(filters) {
const { labels, milestone, author, assignees } = this.processFilters(filters);
this.setFilters({
......@@ -157,12 +146,12 @@ export default {
</script>
<template>
<gl-filtered-search
v-model="value"
:placeholder="__('Filter results')"
:clear-button-title="__('Clear')"
:close-button-title="__('Close')"
:available-tokens="availableTokens"
@submit="filteredSearchSubmit"
<filtered-search-bar
:namespace="groupPath"
recent-searches-storage-key="value-stream-analytics"
:search-input-placeholder="__('Filter results')"
:tokens="tokens"
:initial-filter-value="initialFilterValue()"
@onFilter="handleFilter"
/>
</template>
......@@ -250,10 +250,7 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
commit(types.SET_FEATURE_FLAGS, featureFlags);
if (initialData.group?.fullPath) {
return Promise.resolve()
.then(() =>
dispatch('filters/initialize', { groupPath: initialData.group.fullPath, ...initialData }),
)
return dispatch('filters/initialize', { groupPath: initialData.group.fullPath, ...initialData })
.then(() => dispatch('fetchCycleAnalyticsData'))
.then(() => dispatch('initializeCycleAnalyticsSuccess'));
}
......
......@@ -21,7 +21,10 @@ export const fetchMilestones = ({ commit, state }, search_title = '') => {
return axios
.get(milestonesPath, { params: { search_title } })
.then(({ data }) => commit(types.RECEIVE_MILESTONES_SUCCESS, data))
.then(response => {
commit(types.RECEIVE_MILESTONES_SUCCESS, response.data);
return response;
})
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
......@@ -34,7 +37,10 @@ export const fetchLabels = ({ commit, state }, search = '') => {
return axios
.get(state.labelsPath, { params: { search } })
.then(({ data }) => commit(types.RECEIVE_LABELS_SUCCESS, data))
.then(response => {
commit(types.RECEIVE_LABELS_SUCCESS, response.data);
return response;
})
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_LABELS_ERROR, status);
......@@ -46,7 +52,10 @@ const fetchUser = ({ commit, endpoint, query, action, errorMessage }) => {
commit(`REQUEST_${action}`);
return Api.groupMembers(endpoint, { query })
.then(({ data }) => commit(`RECEIVE_${action}_SUCCESS`, data))
.then(response => {
commit(`RECEIVE_${action}_SUCCESS`, response.data);
return response;
})
.catch(({ response }) => {
const { status } = response;
commit(`RECEIVE_${action}_ERROR`, status);
......@@ -84,7 +93,7 @@ export const setFilters = ({ dispatch }, nextFilters) => {
export const initialize = ({ dispatch, commit }, initialFilters) => {
commit(types.INITIALIZE, initialFilters);
return Promise.resolve()
.then(() => dispatch('setPaths', initialFilters))
.then(() => dispatch('setSelectedFilters', initialFilters, { root: true }));
return dispatch('setPaths', initialFilters).then(() =>
dispatch('setSelectedFilters', initialFilters, { root: true }),
);
};
......@@ -667,11 +667,11 @@ describe('Cycle Analytics component', () => {
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
'project_ids[]': [],
'project_ids[]': null,
milestone_title: null,
author_username: null,
'assignee_username[]': [],
'label_name[]': [],
'assignee_username[]': null,
'label_name[]': null,
};
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id);
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlFilteredSearch } from '@gitlab/ui';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import FilterBar, { prepareTokens } from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import initialFiltersState from 'ee/analytics/cycle_analytics/store/modules/filters/state';
import { filterMilestones, filterLabels } from '../mock_data';
......@@ -42,6 +42,9 @@ describe('Filter bar', () => {
shallowMount(FilterBar, {
localVue,
store: initialStore,
propsData: {
groupPath: 'foo',
},
});
afterEach(() => {
......@@ -51,11 +54,11 @@ describe('Filter bar', () => {
const selectedMilestone = [filterMilestones[0]];
const selectedLabel = [filterLabels[0]];
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const findFilteredSearch = () => wrapper.find(FilteredSearchBar);
const getSearchToken = type =>
findFilteredSearch()
.props('availableTokens')
.filter(token => token.type === type)[0];
.props('tokens')
.find(token => token.type === type);
describe('default', () => {
beforeEach(() => {
......@@ -63,7 +66,7 @@ describe('Filter bar', () => {
wrapper = createComponent(store);
});
it('renders GlFilteredSearch component', () => {
it('renders FilteredSearchBar component', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
});
......@@ -80,7 +83,7 @@ describe('Filter bar', () => {
});
it('displays the milestone and label token', () => {
const tokens = findFilteredSearch().props('availableTokens');
const tokens = findFilteredSearch().props('tokens');
expect(tokens).toHaveLength(4);
expect(tokens[0].type).toBe(milestoneTokenType);
......@@ -89,14 +92,14 @@ describe('Filter bar', () => {
expect(tokens[3].type).toBe(assigneesTokenType);
});
it('displays options in the milestone token', () => {
const { milestones: milestoneToken } = getSearchToken(milestoneTokenType);
it('provides the initial milestone token', () => {
const { initialMilestones: milestoneToken } = getSearchToken(milestoneTokenType);
expect(milestoneToken).toHaveLength(selectedMilestone.length);
});
it('displays options in the label token', () => {
const { labels: labelToken } = getSearchToken(labelsTokenType);
it('provides the initial label token', () => {
const { initialLabels: labelToken } = getSearchToken(labelsTokenType);
expect(labelToken).toHaveLength(selectedLabel.length);
});
......@@ -112,7 +115,7 @@ describe('Filter bar', () => {
});
it('clicks on the search button, setFilters is dispatched', () => {
findFilteredSearch().vm.$emit('submit', [
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } },
{ type: 'labels', value: { data: selectedLabel[0].title, operator: '=' } },
]);
......@@ -130,7 +133,7 @@ describe('Filter bar', () => {
});
it('removes wrapping double quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('submit', [
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: '"milestone with spaces"', operator: '=' } },
]);
......@@ -147,7 +150,7 @@ describe('Filter bar', () => {
});
it('removes wrapping single quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('submit', [
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: "'milestone with spaces'", operator: '=' } },
]);
......@@ -164,7 +167,7 @@ describe('Filter bar', () => {
});
it('does not remove inner double quotes from the data and dispatches setFilters ', () => {
findFilteredSearch().vm.$emit('submit', [
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: 'milestone "with" spaces', operator: '=' } },
]);
......
......@@ -144,7 +144,7 @@ describe('Filters actions', () => {
});
it('dispatches RECEIVE_AUTHORS_SUCCESS with received data', () => {
testAction(
return testAction(
actions.fetchAuthors,
null,
state,
......@@ -153,7 +153,9 @@ describe('Filters actions', () => {
{ type: types.RECEIVE_AUTHORS_SUCCESS, payload: filterUsers },
],
[],
);
).then(({ data }) => {
expect(data).toBe(filterUsers);
});
});
});
......@@ -187,7 +189,7 @@ describe('Filters actions', () => {
});
it('dispatches RECEIVE_MILESTONES_SUCCESS with received data', () => {
testAction(
return testAction(
actions.fetchMilestones,
null,
{ ...state, milestonesPath },
......@@ -196,7 +198,9 @@ describe('Filters actions', () => {
{ type: types.RECEIVE_MILESTONES_SUCCESS, payload: filterMilestones },
],
[],
);
).then(({ data }) => {
expect(data).toBe(filterMilestones);
});
});
});
......@@ -230,7 +234,7 @@ describe('Filters actions', () => {
});
it('dispatches RECEIVE_ASSIGNEES_SUCCESS with received data', () => {
testAction(
return testAction(
actions.fetchAssignees,
null,
{ ...state, milestonesPath },
......@@ -239,7 +243,9 @@ describe('Filters actions', () => {
{ type: types.RECEIVE_ASSIGNEES_SUCCESS, payload: filterUsers },
],
[],
);
).then(({ data }) => {
expect(data).toBe(filterUsers);
});
});
});
......@@ -273,7 +279,7 @@ describe('Filters actions', () => {
});
it('dispatches RECEIVE_LABELS_SUCCESS with received data', () => {
testAction(
return testAction(
actions.fetchLabels,
null,
{ ...state, labelsPath },
......@@ -282,7 +288,9 @@ describe('Filters actions', () => {
{ type: types.RECEIVE_LABELS_SUCCESS, payload: filterLabels },
],
[],
);
).then(({ data }) => {
expect(data).toBe(filterLabels);
});
});
});
......
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