Commit 3a5f1fab authored by Phil Hughes's avatar Phil Hughes

Merge branch '197337-vuex-code-in-base-vue' into 'master'

Remove type of work vuex code from base vue

Closes #197337

See merge request gitlab-org/gitlab!29900
parents a5ae4265 954a50cd
......@@ -69,13 +69,6 @@ export default {
'medians',
'customStageFormErrors',
]),
...mapState('typeOfWork', [
'isLoadingTasksByTypeChart',
'isLoadingTasksByTypeChartTopLabels',
'topRankedLabels',
'subject',
'selectedLabelIds',
]),
...mapGetters([
'hasNoAccessError',
'currentGroupPath',
......@@ -84,7 +77,6 @@ export default {
'enableCustomOrdering',
'cycleAnalyticsRequestParams',
]),
...mapGetters('typeOfWork', ['tasksByTypeChartData']),
shouldRenderEmptyState() {
return !this.selectedGroup;
},
......@@ -106,24 +98,6 @@ export default {
hasDateRangeSet() {
return this.startDate && this.endDate;
},
selectedTasksByTypeFilters() {
const {
selectedGroup,
startDate,
endDate,
selectedProjectIds,
subject,
selectedLabelIds,
} = this;
return {
selectedGroup,
selectedProjectIds,
startDate,
endDate,
subject,
selectedLabelIds,
};
},
query() {
return {
group_id: !this.hideGroupDropDown ? this.currentGroupPath : null,
......@@ -162,7 +136,6 @@ export default {
'updateStage',
'reorderStage',
]),
...mapActions('typeOfWork', ['setTasksByTypeFilters']),
onGroupSelect(group) {
this.setSelectedGroup(group);
this.fetchCycleAnalyticsData();
......@@ -315,13 +288,7 @@ export default {
</div>
</div>
<duration-chart v-if="shouldDisplayDurationChart" class="mt-3" :stages="activeStages" />
<type-of-work-charts
v-if="shouldDisplayTypeOfWorkCharts"
:is-loading="isLoadingTypeOfWork"
:tasks-by-type-chart-data="tasksByTypeChartData"
:selected-tasks-by-type-filters="selectedTasksByTypeFilters"
@updateFilter="setTasksByTypeFilters"
/>
<type-of-work-charts v-if="shouldDisplayTypeOfWorkCharts" :is-loading="isLoadingTypeOfWork" />
</div>
</div>
</template>
......@@ -20,16 +20,10 @@ export default {
required: true,
},
},
computed: {
hasData() {
return Boolean(this.data.length);
},
},
};
</script>
<template>
<gl-stacked-column-chart
v-if="hasData"
:data="data"
:group-by="groupBy"
x-axis-type="category"
......@@ -38,7 +32,4 @@ export default {
:y-axis-title="s__('CycleAnalytics|Number of tasks')"
:series-names="seriesNames"
/>
<div v-else class="bs-callout bs-callout-info">
<p>{{ __('There is no data available. Please change your selection.') }}</p>
</div>
</template>
......@@ -33,6 +33,10 @@ export default {
type: String,
required: true,
},
hasData: {
type: Boolean,
required: true,
},
},
computed: {
subjectFilterOptions() {
......@@ -87,14 +91,13 @@ export default {
TASKS_BY_TYPE_FILTERS,
};
</script>
<template>
<div
class="js-tasks-by-type-chart-filters d-flex flex-row justify-content-between align-items-center"
>
<div class="flex-column">
<h4>{{ s__('CycleAnalytics|Tasks by type') }}</h4>
<p>{{ selectedFiltersText }}</p>
<p v-if="hasData">{{ selectedFiltersText }}</p>
</div>
<div class="flex-column">
<labels-selector
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import TasksByTypeChart from './tasks_by_type/tasks_by_type_chart.vue';
import TasksByTypeFilters from './tasks_by_type/tasks_by_type_filters.vue';
......@@ -9,21 +10,15 @@ import { TASKS_BY_TYPE_SUBJECT_ISSUE } from '../constants';
export default {
name: 'TypeOfWorkCharts',
components: { GlLoadingIcon, TasksByTypeChart, TasksByTypeFilters },
props: {
isLoading: {
type: Boolean,
required: true,
},
tasksByTypeChartData: {
type: Object,
required: true,
},
selectedTasksByTypeFilters: {
type: Object,
required: true,
computed: {
...mapState('typeOfWork', ['isLoadingTasksByTypeChart', 'isLoadingTasksByTypeChartTopLabels']),
...mapGetters('typeOfWork', ['selectedTasksByTypeFilters', 'tasksByTypeChartData']),
hasData() {
return Boolean(this.tasksByTypeChartData?.data.length);
},
isLoading() {
return Boolean(this.isLoadingTasksByTypeChart || this.isLoadingTasksByTypeChartTopLabels);
},
computed: {
summaryDescription() {
const {
startDate,
......@@ -54,6 +49,15 @@ export default {
} = this;
return subject || TASKS_BY_TYPE_SUBJECT_ISSUE;
},
selectedLabelIdsFilter() {
return this.selectedTasksByTypeFilters?.selectedLabelIds || [];
},
},
methods: {
...mapActions('typeOfWork', ['setTasksByTypeFilters']),
onUpdateFilter(e) {
this.setTasksByTypeFilters(e);
},
},
};
</script>
......@@ -64,15 +68,20 @@ export default {
<h3>{{ s__('CycleAnalytics|Type of work') }}</h3>
<p>{{ summaryDescription }}</p>
<tasks-by-type-filters
:selected-label-ids="selectedTasksByTypeFilters.selectedLabelIds"
:has-data="hasData"
:selected-label-ids="selectedLabelIdsFilter"
:subject-filter="selectedSubjectFilter"
@updateFilter="$emit('updateFilter', $event)"
@updateFilter="onUpdateFilter"
/>
<tasks-by-type-chart
v-if="hasData"
:data="tasksByTypeChartData.data"
:group-by="tasksByTypeChartData.groupBy"
:series-names="tasksByTypeChartData.seriesNames"
/>
<div v-else class="bs-callout bs-callout-info">
<p>{{ __('There is no data available. Please change your selection.') }}</p>
</div>
</div>
</div>
</template>
import { getTasksByTypeData } from '../../../utils';
export const selectedTasksByTypeFilters = (state = {}, _, rootState = {}) => {
const { selectedLabelIds = [], subject } = state;
const { selectedGroup, selectedProjectIds = [], startDate = null, endDate = null } = rootState;
return {
selectedGroup,
selectedProjectIds,
startDate,
endDate,
selectedLabelIds,
subject,
};
};
export const tasksByTypeChartData = ({ data = [] } = {}, _, rootState = {}) => {
const { startDate = null, endDate = null } = rootState;
return data.length
......@@ -10,5 +23,3 @@ export const tasksByTypeChartData = ({ data = [] } = {}, _, rootState = {}) => {
})
: { groupBy: [], data: [], seriesNames: [] };
};
export default () => ({ tasksByTypeChartData });
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TasksByTypeChart no data available should render the no data available message 1`] = `
"<div class=\\"bs-callout bs-callout-info\\">
<p>There is no data available. Please change your selection.</p>
</div>"
`;
exports[`TasksByTypeChart no data available should render the no data available message 1`] = `"<gl-stacked-column-chart-stub data=\\"\\" option=\\"[object Object]\\" presentation=\\"stacked\\" groupby=\\"\\" xaxistype=\\"category\\" xaxistitle=\\"Date\\" yaxistitle=\\"Number of tasks\\" seriesnames=\\"\\" legendaveragetext=\\"Avg\\" legendmaxtext=\\"Max\\" y-axis-type=\\"value\\"></gl-stacked-column-chart-stub>"`;
exports[`TasksByTypeChart with data available should render the loading chart 1`] = `"<gl-stacked-column-chart-stub data=\\"0,1,2,5,2,3,2,4,1\\" option=\\"[object Object]\\" presentation=\\"stacked\\" groupby=\\"Group 1,Group 2,Group 3\\" xaxistype=\\"category\\" xaxistitle=\\"Date\\" yaxistitle=\\"Number of tasks\\" seriesnames=\\"Cool label,Normal label\\" legendaveragetext=\\"Avg\\" legendmaxtext=\\"Max\\" y-axis-type=\\"value\\"></gl-stacked-column-chart-stub>"`;
......@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import { GlDropdownItem, GlSegmentedControl } from '@gitlab/ui';
import createFlash from '~/flash';
import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type/tasks_by_type_filters.vue';
import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue';
import {
......@@ -11,7 +12,6 @@ import {
TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { shouldFlashAMessage } from '../../helpers';
import { groupLabels } from '../../mock_data';
import createStore from 'ee/analytics/cycle_analytics/store';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
......@@ -36,7 +36,9 @@ let store = null;
const localVue = createLocalVue();
localVue.use(Vuex);
function createComponent({ props = {}, mountFn = shallowMount }) {
jest.mock('~/flash');
function createComponent({ props = {}, mountFn = shallowMount } = {}) {
store = createStore();
return mountFn(TasksByTypeFilters, {
localVue,
......@@ -51,6 +53,7 @@ function createComponent({ props = {}, mountFn = shallowMount }) {
selectedLabelIds,
labels: groupLabels,
subjectFilter: TASKS_BY_TYPE_SUBJECT_ISSUE,
hasData: true,
...props,
},
stubs: {
......@@ -62,6 +65,7 @@ function createComponent({ props = {}, mountFn = shallowMount }) {
describe('TasksByTypeFilters', () => {
let wrapper = null;
let mock = null;
const selectedFilterText = (count = 1) => `Showing Issues and ${count} labels`;
beforeEach(() => {
mock = mockGroupLabelsRequest();
......@@ -75,7 +79,7 @@ describe('TasksByTypeFilters', () => {
wrapper.destroy();
});
describe('labels', () => {
describe('with data', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({});
......@@ -83,6 +87,31 @@ describe('TasksByTypeFilters', () => {
return waitForPromises();
});
it('renders the selectedFiltersText', () => {
expect(wrapper.text()).toContain(selectedFilterText());
});
});
describe('with no data', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({ props: { hasData: false } });
return waitForPromises();
});
it('renders the selectedFiltersText', () => {
expect(wrapper.text()).not.toContain(selectedFilterText());
});
});
describe('labels', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent();
return waitForPromises();
});
it('emits the `updateFilter` event when a label is selected', () => {
expect(wrapper.emitted('updateFilter')).toBeUndefined();
......@@ -96,7 +125,6 @@ describe('TasksByTypeFilters', () => {
describe('with the warningMessageThreshold label threshold reached', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest();
wrapper = createComponent({
props: {
......@@ -112,11 +140,14 @@ describe('TasksByTypeFilters', () => {
it('should indicate how many labels are selected', () => {
expect(wrapper.text()).toContain('2 selected (5 max)');
});
it('renders the selectedFiltersText', () => {
expect(wrapper.text()).toContain(selectedFilterText(2));
});
});
describe('with maximum labels selected', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest();
wrapper = createComponent({
......@@ -141,7 +172,10 @@ describe('TasksByTypeFilters', () => {
});
it('should display a message', () => {
shouldFlashAMessage('Only 2 labels can be selected at this time');
expect(createFlash).toHaveBeenCalledWith(
'Only 2 labels can be selected at this time',
'notice',
);
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import tasksByTypeStore from 'ee/analytics/cycle_analytics/store/modules/type_of_work';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type/tasks_by_type_chart.vue';
import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type/tasks_by_type_filters.vue';
......@@ -9,18 +11,39 @@ import {
TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
const actionSpies = {
setTasksByTypeFilters: jest.fn(),
};
const fakeStore = ({ initialGetters, initialState }) =>
new Vuex.Store({
modules: {
typeOfWork: {
...tasksByTypeStore,
getters: {
tasksByTypeChartData: () => tasksByTypeData,
selectedTasksByTypeFilters: () => taskByTypeFilters,
...initialGetters,
},
state: {
...initialState,
},
},
},
});
describe('TypeOfWorkCharts', () => {
function createComponent({ props = {}, stubs = {} } = {}) {
function createComponent({ stubs = {}, initialGetters, initialState } = {}) {
return shallowMount(TypeOfWorkCharts, {
propsData: {
isLoading: false,
tasksByTypeChartData: tasksByTypeData,
selectedTasksByTypeFilters: taskByTypeFilters,
...props,
},
localVue,
store: fakeStore({ initialGetters, initialState }),
methods: actionSpies,
stubs: {
TasksByTypeChart: false,
TasksByTypeFilters: false,
TasksByTypeChart: true,
TasksByTypeFilters: true,
...stubs,
},
});
......@@ -31,6 +54,8 @@ describe('TypeOfWorkCharts', () => {
const findSubjectFilters = _wrapper => _wrapper.find(TasksByTypeFilters);
const findTasksByTypeChart = _wrapper => _wrapper.find(TasksByTypeChart);
const findLoader = _wrapper => _wrapper.find(GlLoadingIcon);
const selectedFilterText =
"Type of work Showing data for group 'Gitlab Org' from Dec 11, 2019 to Jan 10, 2020";
afterEach(() => {
wrapper.destroy();
......@@ -45,11 +70,33 @@ describe('TypeOfWorkCharts', () => {
expect(findTasksByTypeChart(wrapper).exists()).toBe(true);
});
it('renders a description of the current filters', () => {
expect(wrapper.text()).toContain(selectedFilterText);
});
it('does not render the loading icon', () => {
expect(findLoader(wrapper).exists()).toBe(false);
});
});
describe('with no data', () => {
beforeEach(() => {
wrapper = createComponent({
initialGetters: {
tasksByTypeChartData: () => ({ groupBy: [], data: [], seriesNames: [] }),
},
});
});
it('does not renders the task by type chart', () => {
expect(findTasksByTypeChart(wrapper).exists()).toBe(false);
});
it('renders the no data available message', () => {
expect(wrapper.text()).toContain('There is no data available. Please change your selection.');
});
});
describe('when a filter is selected', () => {
const payload = {
filter: TASKS_BY_TYPE_FILTERS.SUBJECT,
......@@ -62,15 +109,18 @@ describe('TypeOfWorkCharts', () => {
return wrapper.vm.$nextTick();
});
it('emits the `updateFilter` event', () => {
expect(wrapper.emitted('updateFilter')).toBeDefined();
expect(wrapper.emitted('updateFilter')[0]).toEqual([payload]);
it('calls the setTasksByTypeFilters method', () => {
expect(actionSpies.setTasksByTypeFilters).toHaveBeenCalledWith(payload);
});
});
describe('while loading', () => {
describe.each`
stateKey | value
${'isLoadingTasksByTypeChart'} | ${true}
${'isLoadingTasksByTypeChartTopLabels'} | ${true}
`('when $stateKey=$value', ({ stateKey, value }) => {
beforeEach(() => {
wrapper = createComponent({ props: { isLoading: true } });
wrapper = createComponent({ initialState: { [stateKey]: value } });
});
it('renders loading icon', () => {
......
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