Commit 08cc02f8 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added vuex boilerplate for top ranked labels

Fetch the top labels instead of all labels

Defaults the task by type filter to show only
the selected labels as selected on load

Fix broken specs
parent 13da9216
......@@ -475,6 +475,7 @@ img.emoji {
.mw-90p { max-width: 90%; }
.min-height-0 { min-height: 0; }
.min-width-300 { min-height: 300px; }
.svg-w-100 {
svg {
......
......@@ -64,6 +64,7 @@ export default {
'stages',
'summary',
'labels',
'topRankedLabels',
'currentStageEvents',
'customStageFormEvents',
'errorCode',
......
......@@ -8,6 +8,7 @@ import {
TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE,
TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS,
TASKS_BY_TYPE_MAX_LABELS,
} from '../constants';
export default {
......@@ -60,6 +61,13 @@ export default {
},
);
},
selectedLabelLimitText() {
const { selectedLabelIds } = this;
return sprintf(s__('CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)'), {
selectedLabelsCount: selectedLabelIds.length,
maxLabels: TASKS_BY_TYPE_MAX_LABELS,
});
},
},
mounted() {
$(this.$refs.labelsDropdown).glDropdown({
......@@ -85,14 +93,16 @@ export default {
callback(this.labels);
},
rowTemplate(label) {
const isActiveClass =
this.selectedLabelIds.length && this.selectedLabelIds.includes(label.id) ? 'is-active' : '';
return `
<li>
<a href='#' class='dropdown-menu-link is-active'>
<a href='#' class='dropdown-menu-link ${isActiveClass}'>
<span style="background-color: ${
label.color
};" class="d-inline-block dropdown-label-box">
</span>
${_.escape(label.title)}
${_.escape(label.name)}
</a>
</li>
`;
......@@ -122,7 +132,7 @@ export default {
<icon :size="16" name="settings" />
<icon :size="16" name="chevron-down" />
</gl-button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right">
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right min-width-300">
<div class="js-tasks-by-type-chart-filters-subject mb-3 mx-3">
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
<gl-segmented-control
......@@ -135,15 +145,17 @@ export default {
/>
</div>
<gl-dropdown-divider />
<div class="js-tasks-by-type-chart-filters-labels mb-3 mx-3">
<div class="js-tasks-by-type-chart-filters-labels mb-3 mx-3 w-100">
<p class="font-weight-bold text-left my-2">
{{ s__('CycleAnalytics|Select labels') }}
<br />
<small>{{ selectedLabelLimitText }}</small>
</p>
<div class="dropdown-input px-0">
<input class="dropdown-input-field" type="search" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content px-0"></div>
<div class="dropdown-content px-0 w-100"></div>
</div>
</div>
</div>
......
......@@ -34,6 +34,7 @@ export const DEFAULT_STAGE_NAMES = [...Object.keys(EMPTY_STAGE_TEXT), 'total'];
export const TASKS_BY_TYPE_SUBJECT_ISSUE = 'Issue';
export const TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST = 'MergeRequest';
export const TASKS_BY_TYPE_MAX_LABELS = 15;
export const TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS = {
[TASKS_BY_TYPE_SUBJECT_ISSUE]: __('Issues'),
......
......@@ -172,6 +172,7 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
dispatch('requestCycleAnalyticsData');
return Promise.resolve()
.then(() => dispatch('fetchGroupLabels'))
.then(() => dispatch('fetchTopRankedGroupLabels'))
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('fetchSummaryData'))
......@@ -264,6 +265,45 @@ export const fetchGroupLabels = ({ dispatch, state }) => {
);
};
export const receiveTopRankedGroupLabelsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS, data);
export const receiveTopRankedGroupLabelsError = ({ commit }, error) => {
commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR, error);
createFlash(__('There was an error fetching label data for the selected group'));
};
export const requestTopRankedGroupLabels = ({ commit }) =>
commit(types.REQUEST_TOP_RANKED_GROUP_LABELS);
export const fetchTopRankedGroupLabels = ({
dispatch,
state,
getters: {
currentGroupPath,
cycleAnalyticsRequestParams: { created_after, created_before },
},
}) => {
dispatch('requestTopRankedGroupLabels');
const {
tasksByType: { subject },
} = state;
return Api.cycleAnalyticsTopLabels({
subject,
created_after,
created_before,
group_id: currentGroupPath,
})
.then(({ data }) => dispatch('receiveTopRankedGroupLabelsSuccess', data))
.catch(error =>
handleErrorOrRethrow({
error,
action: () => dispatch('receiveTopRankedGroupLabelsError', error),
}),
);
};
export const receiveGroupStagesAndEventsError = ({ commit }, error) => {
commit(types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR, error);
createFlash(__('There was an error fetching value stream analytics stages.'));
......@@ -371,6 +411,7 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
cycleAnalyticsRequestParams: { created_after, created_before, project_ids },
} = getters;
const {
tasksByType: { labelIds, subject },
} = state;
......
......@@ -28,6 +28,10 @@ export const REQUEST_GROUP_LABELS = 'REQUEST_GROUP_LABELS';
export const RECEIVE_GROUP_LABELS_SUCCESS = 'RECEIVE_GROUP_LABELS_SUCCESS';
export const RECEIVE_GROUP_LABELS_ERROR = 'RECEIVE_GROUP_LABELS_ERROR';
export const REQUEST_TOP_RANKED_GROUP_LABELS = 'REQUEST_TOP_RANKED_GROUP_LABELS';
export const RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS = 'RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS';
export const RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR = 'RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR';
export const REQUEST_SUMMARY_DATA = 'REQUEST_SUMMARY_DATA';
export const RECEIVE_SUMMARY_DATA_SUCCESS = 'RECEIVE_SUMMARY_DATA_SUCCESS';
export const RECEIVE_SUMMARY_DATA_ERROR = 'RECEIVE_SUMMARY_DATA_ERROR';
......
......@@ -58,11 +58,37 @@ export default {
},
[types.REQUEST_GROUP_LABELS](state) {
state.labels = [];
},
[types.RECEIVE_GROUP_LABELS_SUCCESS](state, data = []) {
state.labels = data.map(convertObjectPropsToCamelCase);
},
[types.RECEIVE_GROUP_LABELS_ERROR](state) {
state.labels = [];
},
[types.REQUEST_TOP_RANKED_GROUP_LABELS](state) {
state.topRankedLabels = [];
state.tasksByType = {
...state.tasksByType,
labelIds: [],
};
},
[types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS](state, data = []) {
const { tasksByType } = state;
state.topRankedLabels = data.length ? data.map(convertObjectPropsToCamelCase) : [];
// TODO: Should probably append as many labels from `labels` as we need if we dont get enough returned
state.tasksByType = {
...tasksByType,
labelIds: data.length ? data.map(({ id }) => id) : [],
};
},
[types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR](state) {
const { tasksByType } = state;
state.topRankedLabels = [];
state.tasksByType = {
...tasksByType,
labelIds: [],
};
},
[types.REQUEST_STAGE_MEDIANS](state) {
state.medians = {};
},
......@@ -78,22 +104,6 @@ export default {
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {};
},
[types.RECEIVE_GROUP_LABELS_SUCCESS](state, data = []) {
const { tasksByType } = state;
state.labels = data.map(convertObjectPropsToCamelCase);
state.tasksByType = {
...tasksByType,
labelIds: data.map(({ id }) => id),
};
},
[types.RECEIVE_GROUP_LABELS_ERROR](state) {
const { tasksByType } = state;
state.labels = [];
state.tasksByType = {
...tasksByType,
labelIds: [],
};
},
[types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isCreatingCustomStage = true;
state.isEditingCustomStage = false;
......
......@@ -28,6 +28,7 @@ export default () => ({
stages: [],
summary: [],
labels: [],
topRankedLabels: [],
medians: {},
customStageFormEvents: [],
......
......@@ -15,6 +15,7 @@ export default {
projectPackagesPath: '/api/:version/projects/:id/packages',
projectPackagePath: '/api/:version/projects/:id/packages/:package_id',
cycleAnalyticsTasksByTypePath: '/-/analytics/type_of_work/tasks_by_type',
cycleAnalyticsTopLabelsPath: '/-/analytics/type_of_work/tasks_by_type/top_labels',
cycleAnalyticsSummaryDataPath: '/-/analytics/value_stream_analytics/summary',
cycleAnalyticsGroupStagesAndEventsPath: '/-/analytics/value_stream_analytics/stages',
cycleAnalyticsStageEventsPath: '/-/analytics/value_stream_analytics/stages/:stage_id/records',
......@@ -156,6 +157,11 @@ export default {
return axios.get(url, { params });
},
cycleAnalyticsTopLabels(params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsTopLabelsPath);
return axios.get(url, { params });
},
cycleAnalyticsSummaryData(params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsSummaryDataPath);
return axios.get(url, { params });
......
......@@ -12,41 +12,41 @@ exports[`TasksByTypeChart no data available should render the no data available
`;
exports[`TasksByTypeChart with data available filters labels has label filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 mx-3\\">
"<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 mx-3 w-100\\">
<p class=\\"font-weight-bold text-left my-2\\">
Select labels
</p>
<br> <small>3 selected (15 max)</small></p>
<div class=\\"dropdown-input px-0\\"><input type=\\"search\\" class=\\"dropdown-input-field\\"> <svg aria-hidden=\\"true\\" class=\\"dropdown-input-search s16 ic-search\\" data-hidden=\\"true\\">
<use xlink:href=\\"#search\\"></use>
</svg></div>
<div class=\\"dropdown-content px-0\\"></div>
<div class=\\"dropdown-content px-0 w-100\\"></div>
</div>"
`;
exports[`TasksByTypeChart with data available filters labels with label dropdown open renders the group labels as dropdown items 1`] = `
"<div class=\\"dropdown-content px-0\\">
"<div class=\\"dropdown-content px-0 w-100\\">
<ul>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link is-active\\">
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
<span style=\\"background-color: #FF0000;\\" class=\\"d-inline-block dropdown-label-box\\">
</span>
roses
</a>
</li>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link is-active\\">
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
<span style=\\"background-color: #FFFFFF;\\" class=\\"d-inline-block dropdown-label-box\\">
</span>
some space
</a>
</li>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link is-active\\">
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
<span style=\\"background-color: #0000FF;\\" class=\\"d-inline-block dropdown-label-box\\">
</span>
violets
</a>
</li>
</ul>
......
......@@ -425,6 +425,7 @@ describe('Cycle Analytics component', () => {
mockFetchStageMedian = true,
mockFetchDurationData = true,
mockFetchTasksByTypeData = true,
mockFetchTasksByTypeTopLabelsData = true,
}) {
const defaultStatus = 200;
const defaultRequests = {
......@@ -452,6 +453,12 @@ describe('Cycle Analytics component', () => {
.reply(defaultStatus, { ...mockData.tasksByTypeData });
}
if (mockFetchTasksByTypeTopLabelsData) {
mock
.onGet(mockData.endpoints.tasksByTypeTopLabelsData)
.reply(defaultStatus, mockData.groupLabels);
}
if (mockFetchDurationData) {
mock
.onGet(mockData.endpoints.durationData)
......
......@@ -25,6 +25,7 @@ export const endpoints = {
stageMedian: /analytics\/value_stream_analytics\/stages\/\d+\/median/,
baseStagesEndpoint: /analytics\/value_stream_analytics\/stages$/,
tasksByTypeData: /analytics\/type_of_work\/tasks_by_type/,
tasksByTypeTopLabelsData: /analytics\/type_of_work\/tasks_by_type\/top_labels/,
};
export const groupLabels = getJSONFixture(fixtureEndpoints.groupLabels).map(
......
......@@ -373,6 +373,7 @@ describe('Cycle analytics actions', () => {
[
{ type: 'requestCycleAnalyticsData' },
{ type: 'fetchGroupLabels' },
{ type: 'fetchTopRankedGroupLabels' },
{ type: 'fetchGroupStagesAndEvents' },
{ type: 'fetchStageMedianValues' },
{ type: 'fetchSummaryData' },
......
......@@ -146,19 +146,6 @@ describe('Cycle analytics mutations', () => {
});
});
describe.each`
mutation | value
${types.REQUEST_GROUP_LABELS} | ${[]}
${types.RECEIVE_GROUP_LABELS_ERROR} | ${[]}
`('$mutation', ({ mutation, value }) => {
it(`will set tasksByType.labelIds to ${value}`, () => {
state = { tasksByType: {} };
mutations[mutation](state);
expect(state.tasksByType.labelIds).toEqual(value);
});
});
describe(`${types.RECEIVE_GROUP_LABELS_SUCCESS}`, () => {
it('will set the labels state item with the camelCased group labels', () => {
mutations[types.RECEIVE_GROUP_LABELS_SUCCESS](state, groupLabels);
......
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