Commit 0e0e4478 authored by Martin Wortschack's avatar Martin Wortschack Committed by Phil Hughes

Add deep links

- Set passed params from BE as initial data on the store
and update URL with query params when filters change
parent 8fd29cd8
...@@ -10,9 +10,21 @@ export default { ...@@ -10,9 +10,21 @@ export default {
GroupsDropdownFilter, GroupsDropdownFilter,
ProjectsDropdownFilter, ProjectsDropdownFilter,
}, },
props: {
group: {
type: Object,
required: false,
default: null,
},
project: {
type: Object,
required: false,
default: null,
},
},
data() { data() {
return { return {
groupId: null, groupId: this.group && this.group.id ? this.group.id : null,
}; };
}, },
computed: { computed: {
...@@ -20,6 +32,9 @@ export default { ...@@ -20,6 +32,9 @@ export default {
showProjectsDropdownFilter() { showProjectsDropdownFilter() {
return Boolean(this.groupId); return Boolean(this.groupId);
}, },
projects() {
return this.project && Object.keys(this.project).length ? [this.project] : null;
},
}, },
methods: { methods: {
...mapActions('filters', ['setGroupNamespace', 'setProjectPath']), ...mapActions('filters', ['setGroupNamespace', 'setProjectPath']),
...@@ -62,12 +77,14 @@ export default { ...@@ -62,12 +77,14 @@ export default {
<groups-dropdown-filter <groups-dropdown-filter
class="group-select" class="group-select"
:query-params="$options.groupsQueryParams" :query-params="$options.groupsQueryParams"
:default-group="group"
@selected="onGroupSelected" @selected="onGroupSelected"
/> />
<projects-dropdown-filter <projects-dropdown-filter
v-if="showProjectsDropdownFilter" v-if="showProjectsDropdownFilter"
:key="groupId" :key="groupId"
class="project-select" class="project-select"
:default-projects="projects"
:query-params="$options.projectsQueryParams" :query-params="$options.projectsQueryParams"
:group-id="groupId" :group-id="groupId"
@selected="onProjectsSelected" @selected="onProjectsSelected"
......
import Vue from 'vue'; import Vue from 'vue';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { defaultDaysInPast } from './constants';
import store from './store'; import store from './store';
import FilterDropdowns from './components/filter_dropdowns.vue'; import FilterDropdowns from './components/filter_dropdowns.vue';
import DateRange from '../shared/components/daterange.vue'; import DateRange from '../shared/components/daterange.vue';
import ProductivityAnalyticsApp from './components/app.vue'; import ProductivityAnalyticsApp from './components/app.vue';
import FilteredSearchProductivityAnalytics from './filtered_search_productivity_analytics'; import FilteredSearchProductivityAnalytics from './filtered_search_productivity_analytics';
import { getLabelsEndpoint, getMilestonesEndpoint, getDefaultStartDate } from './utils'; import {
getLabelsEndpoint,
getMilestonesEndpoint,
buildGroupFromDataset,
buildProjectFromDataset,
} from './utils';
export default () => { export default () => {
const container = document.getElementById('js-productivity-analytics'); const container = document.getElementById('js-productivity-analytics');
...@@ -18,19 +22,45 @@ export default () => { ...@@ -18,19 +22,45 @@ export default () => {
const timeframeContainer = container.querySelector('.js-timeframe-container'); const timeframeContainer = container.querySelector('.js-timeframe-container');
const appContainer = container.querySelector('.js-productivity-analytics-app-container'); const appContainer = container.querySelector('.js-productivity-analytics-app-container');
const {
authorUsername,
labelName,
milestoneTitle,
mergedAtAfter,
mergedAtBefore,
} = container.dataset;
const mergedAtAfterDate = new Date(mergedAtAfter);
const mergedAtBeforeDate = new Date(mergedAtBefore);
const { endpoint, emptyStateSvgPath, noAccessSvgPath } = appContainer.dataset; const { endpoint, emptyStateSvgPath, noAccessSvgPath } = appContainer.dataset;
const { startDate: computedStartDate } = timeframeContainer.dataset; const minDate = timeframeContainer.dataset.startDate
? new Date(timeframeContainer.dataset.startDate)
: null;
const minDate = computedStartDate ? new Date(computedStartDate) : null; const group = buildGroupFromDataset(container.dataset);
const mergedAtAfter = getDefaultStartDate(minDate, defaultDaysInPast); let project = null;
const mergedAtBefore = new Date(Date.now());
const initialData = { let initialData = {
mergedAtAfter, mergedAtAfter: mergedAtAfterDate,
mergedAtBefore, mergedAtBefore: mergedAtBeforeDate,
minDate, minDate,
}; };
// let's set the initial data (from URL query params) only if we receive a valid group from BE
if (group) {
project = buildProjectFromDataset(container.dataset);
initialData = {
...initialData,
groupNamespace: group.full_path,
projectPath: project ? project.path_with_namespace : null,
authorUsername,
labelName: labelName ? labelName.split(',') : null,
milestoneTitle,
};
}
let filterManager; let filterManager;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -38,11 +68,24 @@ export default () => { ...@@ -38,11 +68,24 @@ export default () => {
el: groupProjectSelectContainer, el: groupProjectSelectContainer,
store, store,
created() { created() {
// let's not fetch any data by default since we might not have a valid group yet
let skipFetch = true;
this.setEndpoint(endpoint); this.setEndpoint(endpoint);
// let's not fetch data since we might not have a groupNamespace selected yet if (group) {
// this just populates the store with the initial data and waits for a groupNamespace to be set this.initFilteredSearch({
this.setInitialData({ skipFetch: true, data: initialData }); groupNamespace: group.full_path,
groupId: group.id,
projectNamespace: project ? project.path_with_namespace : null,
projectId: project ? project.id : null,
});
// let's fetch data now since we do have a valid group
skipFetch = false;
}
this.setInitialData({ skipFetch, data: initialData });
}, },
methods: { methods: {
...mapActions(['setEndpoint']), ...mapActions(['setEndpoint']),
...@@ -80,6 +123,10 @@ export default () => { ...@@ -80,6 +123,10 @@ export default () => {
}, },
render(h) { render(h) {
return h(FilterDropdowns, { return h(FilterDropdowns, {
props: {
group,
project,
},
on: { on: {
groupSelected: this.onGroupSelected, groupSelected: this.onGroupSelected,
projectSelected: this.onProjectSelected, projectSelected: this.onProjectSelected,
...@@ -105,8 +152,8 @@ export default () => { ...@@ -105,8 +152,8 @@ export default () => {
return h(DateRange, { return h(DateRange, {
props: { props: {
show: this.groupNamespace !== null, show: this.groupNamespace !== null,
startDate: mergedAtAfter, startDate: mergedAtAfterDate,
endDate: mergedAtBefore, endDate: mergedAtBeforeDate,
minDate, minDate,
}, },
on: { on: {
......
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { chartKeys } from '../../../constants'; import { chartKeys } from '../../../constants';
...@@ -16,6 +18,8 @@ export const setInitialData = ({ commit, dispatch }, { skipFetch = false, data } ...@@ -16,6 +18,8 @@ export const setInitialData = ({ commit, dispatch }, { skipFetch = false, data }
export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => { export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => {
commit(types.SET_GROUP_NAMESPACE, groupNamespace); commit(types.SET_GROUP_NAMESPACE, groupNamespace);
historyPushState(setUrlParams({ group_id: groupNamespace }, window.location.href, true));
// let's reset the current selection first // let's reset the current selection first
// with skipReload=true we avoid data from being fetched here // with skipReload=true we avoid data from being fetched here
dispatch('charts/resetMainChartSelection', true, { root: true }); dispatch('charts/resetMainChartSelection', true, { root: true });
...@@ -29,9 +33,17 @@ export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => { ...@@ -29,9 +33,17 @@ export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => {
}); });
}; };
export const setProjectPath = ({ commit, dispatch }, projectPath) => { export const setProjectPath = ({ commit, dispatch, state }, projectPath) => {
commit(types.SET_PROJECT_PATH, projectPath); commit(types.SET_PROJECT_PATH, projectPath);
historyPushState(
setUrlParams(
{ group_id: state.groupNamespace, project_id: projectPath },
window.location.href,
true,
),
);
dispatch('charts/resetMainChartSelection', true, { root: true }); dispatch('charts/resetMainChartSelection', true, { root: true });
return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => { return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => {
...@@ -51,6 +63,8 @@ export const setFilters = ( ...@@ -51,6 +63,8 @@ export const setFilters = (
milestoneTitle: milestone_title, milestoneTitle: milestone_title,
}); });
historyPushState(setUrlParams({ author_username, 'label_name[]': label_name, milestone_title }));
dispatch('charts/resetMainChartSelection', true, { root: true }); dispatch('charts/resetMainChartSelection', true, { root: true });
return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => { return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => {
...@@ -63,6 +77,8 @@ export const setFilters = ( ...@@ -63,6 +77,8 @@ export const setFilters = (
export const setDateRange = ({ commit, dispatch }, { startDate, endDate }) => { export const setDateRange = ({ commit, dispatch }, { startDate, endDate }) => {
commit(types.SET_DATE_RANGE, { startDate, endDate }); commit(types.SET_DATE_RANGE, { startDate, endDate });
historyPushState(setUrlParams({ merged_at_after: startDate, merged_at_before: endDate }));
dispatch('charts/resetMainChartSelection', true, { root: true }); dispatch('charts/resetMainChartSelection', true, { root: true });
return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => { return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => {
......
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_INITIAL_DATA](state, { mergedAtAfter, mergedAtBefore, minDate }) { [types.SET_INITIAL_DATA](
state,
{
groupNamespace = null,
projectPath = null,
authorUsername = null,
labelName = [],
milestoneTitle = null,
mergedAtAfter,
mergedAtBefore,
minDate,
},
) {
state.groupNamespace = groupNamespace;
state.projectPath = projectPath;
state.authorUsername = authorUsername;
state.labelName = labelName;
state.milestoneTitle = milestoneTitle;
state.startDate = mergedAtAfter; state.startDate = mergedAtAfter;
state.endDate = mergedAtBefore; state.endDate = mergedAtBefore;
state.minDate = minDate; state.minDate = minDate;
...@@ -9,9 +26,15 @@ export default { ...@@ -9,9 +26,15 @@ export default {
[types.SET_GROUP_NAMESPACE](state, groupNamespace) { [types.SET_GROUP_NAMESPACE](state, groupNamespace) {
state.groupNamespace = groupNamespace; state.groupNamespace = groupNamespace;
state.projectPath = null; state.projectPath = null;
state.authorUsername = null;
state.labelName = [];
state.milestoneTitle = null;
}, },
[types.SET_PROJECT_PATH](state, projectPath) { [types.SET_PROJECT_PATH](state, projectPath) {
state.projectPath = projectPath; state.projectPath = projectPath;
state.authorUsername = null;
state.labelName = [];
state.milestoneTitle = null;
}, },
[types.SET_FILTERS](state, { authorUsername, labelName, milestoneTitle }) { [types.SET_FILTERS](state, { authorUsername, labelName, milestoneTitle }) {
state.authorUsername = authorUsername; state.authorUsername = authorUsername;
......
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import * as actions from 'ee/analytics/productivity_analytics/store/modules/filters/actions'; import * as actions from 'ee/analytics/productivity_analytics/store/modules/filters/actions';
import * as types from 'ee/analytics/productivity_analytics/store/modules/filters/mutation_types'; import * as types from 'ee/analytics/productivity_analytics/store/modules/filters/mutation_types';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { chartKeys } from 'ee/analytics/productivity_analytics/constants'; import { chartKeys } from 'ee/analytics/productivity_analytics/constants';
import getInitialState from 'ee/analytics/productivity_analytics/store/modules/filters/state'; import getInitialState from 'ee/analytics/productivity_analytics/store/modules/filters/state';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/lib/utils/url_utility');
describe('Productivity analytics filter actions', () => { describe('Productivity analytics filter actions', () => {
let store; let store;
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
...@@ -21,9 +26,16 @@ describe('Productivity analytics filter actions', () => { ...@@ -21,9 +26,16 @@ describe('Productivity analytics filter actions', () => {
store = { store = {
commit: jest.fn(), commit: jest.fn(),
dispatch: jest.fn(() => Promise.resolve()), dispatch: jest.fn(() => Promise.resolve()),
state: {
groupNamespace,
},
}; };
}); });
afterEach(() => {
setUrlParams.mockClear();
});
describe('setInitialData', () => { describe('setInitialData', () => {
it('commits the SET_INITIAL_DATA mutation and fetches data by default', done => { it('commits the SET_INITIAL_DATA mutation and fetches data by default', done => {
actions actions
...@@ -95,6 +107,21 @@ describe('Productivity analytics filter actions', () => { ...@@ -95,6 +107,21 @@ describe('Productivity analytics filter actions', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('calls setUrlParams with the group_id param', done => {
actions
.setGroupNamespace(store, groupNamespace)
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith(
{ group_id: groupNamespace },
window.location.href,
true,
);
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
}); });
describe('setProjectPath', () => { describe('setProjectPath', () => {
...@@ -127,6 +154,21 @@ describe('Productivity analytics filter actions', () => { ...@@ -127,6 +154,21 @@ describe('Productivity analytics filter actions', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('calls setUrlParams with the group_id and project_id params', done => {
actions
.setProjectPath(store, projectPath)
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith(
{ group_id: groupNamespace, project_id: projectPath },
window.location.href,
true,
);
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
}); });
describe('setFilters', () => { describe('setFilters', () => {
...@@ -159,6 +201,17 @@ describe('Productivity analytics filter actions', () => { ...@@ -159,6 +201,17 @@ describe('Productivity analytics filter actions', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('calls setUrlParams with the author_username', done => {
actions
.setFilters(store, { author_username: 'root' })
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith({ author_username: 'root' });
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
}); });
describe('setDateRange', () => { describe('setDateRange', () => {
...@@ -191,5 +244,20 @@ describe('Productivity analytics filter actions', () => { ...@@ -191,5 +244,20 @@ describe('Productivity analytics filter actions', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('calls setUrlParams with the merged_at_after=startDate and merged_at_before=endDate', done => {
actions
.setDateRange(store, { startDate, endDate })
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith({
merged_at_after: startDate,
merged_at_before: endDate,
});
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
}); });
}); });
...@@ -21,12 +21,22 @@ describe('Productivity analytics filter mutations', () => { ...@@ -21,12 +21,22 @@ describe('Productivity analytics filter mutations', () => {
describe(types.SET_INITIAL_DATA, () => { describe(types.SET_INITIAL_DATA, () => {
it('sets the initial data', () => { it('sets the initial data', () => {
const initialData = { const initialData = {
groupNamespace,
projectPath,
authorUsername,
labelName,
milestoneTitle,
mergedAtAfter: startDate, mergedAtAfter: startDate,
mergedAtBefore: endDate, mergedAtBefore: endDate,
minDate, minDate,
}; };
mutations[types.SET_INITIAL_DATA](state, initialData); mutations[types.SET_INITIAL_DATA](state, initialData);
expect(state.groupNamespace).toBe(groupNamespace);
expect(state.projectPath).toBe(projectPath);
expect(state.authorUsername).toBe(authorUsername);
expect(state.labelName).toEqual(labelName);
expect(state.milestoneTitle).toBe(milestoneTitle);
expect(state.startDate).toBe(startDate); expect(state.startDate).toBe(startDate);
expect(state.endDate).toBe(endDate); expect(state.endDate).toBe(endDate);
expect(state.minDate).toBe(minDate); expect(state.minDate).toBe(minDate);
...@@ -38,6 +48,10 @@ describe('Productivity analytics filter mutations', () => { ...@@ -38,6 +48,10 @@ describe('Productivity analytics filter mutations', () => {
mutations[types.SET_GROUP_NAMESPACE](state, groupNamespace); mutations[types.SET_GROUP_NAMESPACE](state, groupNamespace);
expect(state.groupNamespace).toBe(groupNamespace); expect(state.groupNamespace).toBe(groupNamespace);
expect(state.projectPath).toBe(null);
expect(state.authorUsername).toBe(null);
expect(state.labelName).toEqual([]);
expect(state.milestoneTitle).toBe(null);
}); });
}); });
...@@ -46,6 +60,9 @@ describe('Productivity analytics filter mutations', () => { ...@@ -46,6 +60,9 @@ describe('Productivity analytics filter mutations', () => {
mutations[types.SET_PROJECT_PATH](state, projectPath); mutations[types.SET_PROJECT_PATH](state, projectPath);
expect(state.projectPath).toBe(projectPath); expect(state.projectPath).toBe(projectPath);
expect(state.authorUsername).toBe(null);
expect(state.labelName).toEqual([]);
expect(state.milestoneTitle).toBe(null);
}); });
}); });
......
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