Commit b5832553 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'mw-pa-deep-links-fe' into 'master'

Productivity Analytics: Add deep links (FE only)

Closes #32423

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