Commit b169a4c1 authored by Doug Stull's avatar Doug Stull

Merge branch '198592-add-group-labels-to-vuex-store' into 'master'

Store fetched group labels in vuex

See merge request gitlab-org/gitlab!64273
parents 781b778f d90ee4bb
<script>
import { GlFormGroup, GlFormInput, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { isLabelEvent, getLabelEventsIdentifiers } from '../../utils';
import { isLabelEvent, getLabelEventsIdentifiers, uniqById } from '../../utils';
import LabelsSelector from '../labels_selector.vue';
import { i18n } from './constants';
import StageFieldActions from './stage_field_actions.vue';
......@@ -38,6 +38,11 @@ export default {
type: Array,
required: true,
},
defaultGroupLabels: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
......@@ -69,6 +74,15 @@ export default {
selectedEndEventName() {
return this.eventName(this.stage.endEventIdentifier, 'SELECT_END_EVENT');
},
initialGroupLabels() {
return uniqById(
[
this.stage.startEventLabelId ? this.stage.startEventLabel : null,
this.stage.endEventLabelId ? this.stage.endEventLabel : null,
...this.defaultGroupLabels,
].filter((l) => Boolean(l)),
);
},
},
methods: {
hasFieldErrors(key) {
......@@ -150,7 +164,8 @@ export default {
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:selected-label-id="[stage.startEventLabelId]"
:initial-data="initialGroupLabels"
:selected-label-ids="[stage.startEventLabelId]"
:name="`custom-stage-start-label-${index}`"
@select-label="$emit('input', { field: 'startEventLabelId', value: $event })"
/>
......@@ -193,7 +208,8 @@ export default {
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:selected-label-id="[stage.endEventLabelId]"
:initial-data="initialGroupLabels"
:selected-label-ids="[stage.endEventLabelId]"
:name="`custom-stage-end-label-${index}`"
@select-label="$emit('input', { field: 'endEventLabelId', value: $event })"
/>
......
......@@ -191,6 +191,8 @@ const findStageByName = (stages, targetName = '') =>
*/
const prepareCustomStage = ({ startEventLabel = {}, endEventLabel = {}, ...rest }) => ({
...rest,
startEventLabel,
endEventLabel,
startEventLabelId: startEventLabel?.id || null,
endEventLabelId: endEventLabel?.id || null,
isDefault: false,
......
......@@ -26,7 +26,7 @@ export default {
GlSearchBoxByType,
},
props: {
defaultSelectedLabelIds: {
initialData: {
type: Array,
required: false,
default: () => [],
......@@ -46,7 +46,7 @@ export default {
required: false,
default: false,
},
selectedLabelId: {
selectedLabelIds: {
type: Array,
required: false,
default: () => [],
......@@ -67,14 +67,13 @@ export default {
loading: false,
searchTerm: '',
labels: [],
selectedLabelIds: this.defaultSelectedLabelIds || [],
};
},
computed: {
selectedLabel() {
const { selectedLabelId, labels = [] } = this;
if (!selectedLabelId.length || !labels.length) return null;
return labels.find(({ id }) => selectedLabelId.includes(id));
const { selectedLabelIds, labels = [] } = this;
if (!selectedLabelIds.length || !labels.length) return null;
return labels.find(({ id }) => selectedLabelIds.includes(id));
},
maxLabelsSelected() {
return this.selectedLabelIds.length >= this.maxLabels;
......@@ -89,7 +88,11 @@ export default {
},
},
mounted() {
if (!this.initialData.length) {
this.fetchData();
} else {
this.labels = this.initialData;
}
},
methods: {
...mapGetters(['currentGroupPath']),
......@@ -121,7 +124,7 @@ export default {
return label?.name || label.title;
},
isSelectedLabel(id) {
return Boolean(this.selectedLabelId?.includes(id));
return Boolean(this.selectedLabelIds?.includes(id));
},
isDisabledLabel(id) {
return Boolean(this.maxLabelsSelected && !this.isSelectedLabel(id));
......
......@@ -38,6 +38,11 @@ export default {
type: Boolean,
required: true,
},
defaultGroupLabels: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
subjectFilterOptions() {
......@@ -108,10 +113,10 @@ export default {
<div class="flex-column">
<labels-selector
data-testid="type-of-work-filters-label"
:default-selected-labels-ids="selectedLabelIds"
:initial-data="defaultGroupLabels"
:max-labels="maxLabels"
:aria-label="__('CycleAnalytics|Display chart filters')"
:selected-label-id="selectedLabelIds"
:selected-label-ids="selectedLabelIds"
aria-expanded="false"
multiselect
right
......
......@@ -5,6 +5,7 @@ import { s__, sprintf, __ } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { formattedDate } from '../../shared/utils';
import { TASKS_BY_TYPE_SUBJECT_ISSUE } from '../constants';
import { uniqById } from '../utils';
import TasksByTypeChart from './tasks_by_type/tasks_by_type_chart.vue';
import TasksByTypeFilters from './tasks_by_type/tasks_by_type_filters.vue';
......@@ -16,6 +17,7 @@ export default {
'isLoadingTasksByTypeChart',
'isLoadingTasksByTypeChartTopLabels',
'errorMessage',
'topRankedLabels',
]),
...mapGetters('typeOfWork', ['selectedTasksByTypeFilters', 'tasksByTypeChartData']),
hasData() {
......@@ -62,6 +64,9 @@ export default {
? this.errorMessage
: __('There is no data available. Please change your selection.');
},
initialGroupLabels() {
return uniqById(this.topRankedLabels);
},
},
methods: {
...mapActions('typeOfWork', ['setTasksByTypeFilters']),
......@@ -78,6 +83,7 @@ export default {
<h3>{{ s__('CycleAnalytics|Type of work') }}</h3>
<p>{{ summaryDescription }}</p>
<tasks-by-type-filters
:default-group-labels="initialGroupLabels"
:has-data="hasData"
:selected-label-ids="selectedLabelIdsFilter"
:subject-filter="selectedSubjectFilter"
......
<script>
import { GlButton, GlForm, GlFormInput, GlFormGroup, GlFormRadioGroup, GlModal } from '@gitlab/ui';
import {
GlButton,
GlForm,
GlFormInput,
GlFormGroup,
GlFormRadioGroup,
GlLoadingIcon,
GlModal,
} from '@gitlab/ui';
import { cloneDeep, uniqueId } from 'lodash';
import Vue from 'vue';
import { mapState, mapActions } from 'vuex';
......@@ -43,6 +51,7 @@ export default {
GlFormInput,
GlFormGroup,
GlFormRadioGroup,
GlLoadingIcon,
GlModal,
DefaultStageFields,
CustomStageFields,
......@@ -101,7 +110,12 @@ export default {
};
},
computed: {
...mapState({ isCreating: 'isCreatingValueStream', formEvents: 'formEvents' }),
...mapState({
isCreating: 'isCreatingValueStream',
isFetchingGroupLabels: 'isFetchingGroupLabels',
formEvents: 'formEvents',
defaultGroupLabels: 'defaultGroupLabels',
}),
isValueStreamNameValid() {
return !this.nameError?.length;
},
......@@ -149,8 +163,13 @@ export default {
return this.defaultStageConfig.map(({ name }) => name);
},
},
created() {
if (!this.defaultGroupLabels) {
this.fetchGroupLabels();
}
},
methods: {
...mapActions(['createValueStream', 'updateValueStream']),
...mapActions(['createValueStream', 'updateValueStream', 'fetchGroupLabels']),
onSubmit() {
this.validate();
if (this.hasFormErrors) return false;
......@@ -316,7 +335,8 @@ export default {
@secondary.prevent="onAddStage"
@primary.prevent="onSubmit"
>
<gl-form>
<gl-loading-icon v-if="isFetchingGroupLabels" size="lg" color="dark" class="gl-my-12" />
<gl-form v-else>
<gl-form-group
data-testid="create-value-stream-name"
label-for="create-value-stream-name"
......@@ -373,6 +393,7 @@ export default {
:index="activeStageIndex"
:total-stages="stages.length"
:errors="fieldErrors(activeStageIndex)"
:default-group-labels="defaultGroupLabels"
@move="handleMove"
@remove="onRemove"
@input="onFieldInput(activeStageIndex, $event)"
......
import Api from 'ee/api';
import { removeFlash } from '~/cycle_analytics/utils';
import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
......@@ -23,6 +24,13 @@ export const setPaths = ({ dispatch }, options) => {
export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags);
export const fetchGroupLabels = ({ commit, getters: { currentGroupPath } }) => {
commit(types.REQUEST_GROUP_LABELS);
return Api.cycleAnalyticsGroupLabels(currentGroupPath, { only_group_labels: true })
.then(({ data = [] }) => commit(types.RECEIVE_GROUP_LABELS_SUCCESS, data))
.catch(() => commit(types.RECEIVE_GROUP_LABELS_ERROR));
};
export const requestCycleAnalyticsData = ({ commit }) => commit(types.REQUEST_VALUE_STREAM_DATA);
export const receiveCycleAnalyticsDataSuccess = ({ commit, dispatch }) => {
......
......@@ -27,6 +27,10 @@ export const REQUEST_GROUP_STAGES = 'REQUEST_GROUP_STAGES';
export const RECEIVE_GROUP_STAGES_SUCCESS = 'RECEIVE_GROUP_STAGES_SUCCESS';
export const RECEIVE_GROUP_STAGES_ERROR = 'RECEIVE_GROUP_STAGES_ERROR';
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 INITIALIZE_VSA = 'INITIALIZE_VSA';
export const INITIALIZE_VALUE_STREAM_SUCCESS = 'INITIALIZE_VALUE_STREAM_SUCCESS';
......
......@@ -87,6 +87,18 @@ export default {
[types.RECEIVE_GROUP_STAGES_SUCCESS](state, stages) {
state.stages = transformRawStages(stages);
},
[types.REQUEST_GROUP_LABELS](state) {
state.isFetchingGroupLabels = true;
state.defaultGroupLabels = [];
},
[types.RECEIVE_GROUP_LABELS_ERROR](state) {
state.isFetchingGroupLabels = false;
state.defaultGroupLabels = [];
},
[types.RECEIVE_GROUP_LABELS_SUCCESS](state, groupLabels = []) {
state.isFetchingGroupLabels = false;
state.defaultGroupLabels = groupLabels.map(convertObjectPropsToCamelCase);
},
[types.INITIALIZE_VSA](
state,
{
......
......@@ -6,6 +6,7 @@ import {
export default () => ({
featureFlags: {},
defaultStageConfig: [],
defaultGroupLabels: null,
createdAfter: null,
createdBefore: null,
......@@ -26,6 +27,7 @@ export default () => ({
isCreatingValueStream: false,
isEditingValueStream: false,
isDeletingValueStream: false,
isFetchingGroupLabels: false,
createValueStreamErrors: {},
deleteValueStreamError: null,
......
import dateFormat from 'dateformat';
import { isNumber } from 'lodash';
import { isNumber, uniqBy } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils';
......@@ -371,3 +371,11 @@ export const formatMedianValuesWithOverview = (medians = []) => {
[OVERVIEW_STAGE_ID]: overviewMedian ? medianTimeToParsedSeconds(overviewMedian) : '-',
};
};
/**
* Takes an array of objects with potential duplicates and returns the deduplicated array
*
* @param {Array} arr - The array of objects with potential duplicates
* @returns {Array} The unique objects from the original array
*/
export const uniqById = (arr = []) => uniqBy(arr, ({ id }) => id);
# frozen_string_literal: true
module EE::Groups::Analytics::CycleAnalyticsHelper
include Analytics::CycleAnalyticsHelper
def group_cycle_analytics_data(group)
api_paths = group.present? ? cycle_analytics_api_paths(group) : {}
image_paths = cycle_analytics_image_paths
default_stages = { default_stages: cycle_analytics_default_stage_config.to_json }
api_paths.merge(image_paths, default_stages)
end
private
def cycle_analytics_image_paths
{
empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"),
no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"),
no_access_svg_path: image_path("illustrations/analytics/no-access.svg")
}
end
def cycle_analytics_api_paths(group)
{ milestones_path: group_milestones_path(group), labels_path: group_labels_path(group) }
end
end
- page_title _("Value Stream Analytics")
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
- api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group) } : {}
- image_paths = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg")}
- default_stages = { default_stages: cycle_analytics_default_stage_config.to_json }
- data_attributes.merge!(api_paths, image_paths, default_stages)
- data_attributes.merge!(group_cycle_analytics_data(@group))
- add_page_specific_style 'page_bundles/cycle_analytics'
#js-cycle-analytics-app{ data: data_attributes }
......@@ -29,7 +29,7 @@ exports[`Value Stream Analytics LabelsSelector with no item selected will render
</gl-dropdown-stub>"
`;
exports[`Value Stream Analytics LabelsSelector with selectedLabelId set will render the label selector 1`] = `
exports[`Value Stream Analytics LabelsSelector with selectedLabelIds set will render the label selector 1`] = `
"<gl-dropdown-stub headertext=\\"\\" hideheaderborder=\\"true\\" text=\\"\\" category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" toggleclass=\\"gl-overflow-hidden\\" class=\\"gl-w-full\\">
<gl-dropdown-section-header-stub>Select a label </gl-dropdown-section-header-stub>
<div class=\\"mb-3 px-3\\">
......
import { GlDropdownSectionHeader } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vuex from 'vuex';
import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue';
import createStore from 'ee/analytics/cycle_analytics/store';
......@@ -11,6 +12,7 @@ import createFlash from '~/flash';
import { groupLabels } from '../mock_data';
jest.mock('~/flash');
Vue.use(Vuex);
const selectedLabel = groupLabels[groupLabels.length - 1];
const findActiveItem = (wrapper) =>
......@@ -24,14 +26,11 @@ const mockGroupLabelsRequest = (status = 200) =>
describe('Value Stream Analytics LabelsSelector', () => {
let store = null;
const localVue = createLocalVue();
localVue.use(Vuex);
function createComponent({ props = { selectedLabelId: [] }, shallow = true } = {}) {
function createComponent({ props = { selectedLabelIds: [] }, shallow = true } = {}) {
store = createStore();
const func = shallow ? shallowMount : mount;
return func(LabelsSelector, {
localVue,
store: {
...store,
getters: {
......@@ -71,6 +70,10 @@ describe('Value Stream Analytics LabelsSelector', () => {
expect(wrapper.text()).toContain(name);
});
it('will fetch the labels', () => {
expect(mock.history.get.length).toBe(1);
});
it('will render with the default option selected', () => {
const sectionHeader = wrapper.findComponent(GlDropdownSectionHeader);
......@@ -114,10 +117,10 @@ describe('Value Stream Analytics LabelsSelector', () => {
});
});
describe('with selectedLabelId set', () => {
describe('with selectedLabelIds set', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({ props: { selectedLabelId: [selectedLabel.id] } });
wrapper = createComponent({ props: { selectedLabelIds: [selectedLabel.id] } });
return waitForPromises();
});
......@@ -136,4 +139,19 @@ describe('Value Stream Analytics LabelsSelector', () => {
expect(activeItem.text()).toEqual(selectedLabel.name);
});
});
describe('with labels provided', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({ props: { initialData: groupLabels } });
});
afterEach(() => {
wrapper.destroy();
});
it('will not fetch the labels', () => {
expect(mock.history.get.length).toBe(0);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
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';
......@@ -8,10 +9,18 @@ import {
TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { tasksByTypeData, taskByTypeFilters } from '../mock_data';
import { tasksByTypeData, taskByTypeFilters, groupLabels } from '../mock_data';
const fakeTopRankedLabels = [
...groupLabels,
{
...groupLabels[0],
id: 1337,
name: 'fake label',
},
];
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
const actionSpies = {
setTasksByTypeFilters: jest.fn(),
......@@ -19,6 +28,9 @@ const actionSpies = {
const fakeStore = ({ initialGetters, initialState }) =>
new Vuex.Store({
state: {
defaultGroupLabels: groupLabels,
},
modules: {
typeOfWork: {
namespaced: true,
......@@ -29,6 +41,7 @@ const fakeStore = ({ initialGetters, initialState }) =>
...initialGetters,
},
state: {
topRankedLabels: [],
...initialState,
},
actions: actionSpies,
......@@ -39,7 +52,6 @@ const fakeStore = ({ initialGetters, initialState }) =>
describe('TypeOfWorkCharts', () => {
function createComponent({ stubs = {}, initialGetters, initialState } = {}) {
return shallowMount(TypeOfWorkCharts, {
localVue,
store: fakeStore({ initialGetters, initialState }),
stubs: {
TasksByTypeChart: true,
......@@ -51,6 +63,7 @@ describe('TypeOfWorkCharts', () => {
let wrapper = null;
const labelIds = (labels) => labels.map(({ id }) => id);
const findSubjectFilters = (_wrapper) => _wrapper.findComponent(TasksByTypeFilters);
const findTasksByTypeChart = (_wrapper) => _wrapper.findComponent(TasksByTypeChart);
const findLoader = (_wrapper) => _wrapper.findComponent(ChartSkeletonLoader);
......@@ -77,6 +90,18 @@ describe('TypeOfWorkCharts', () => {
it('does not render the loading icon', () => {
expect(findLoader(wrapper).exists()).toBe(false);
});
describe('with topRankedLabels', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { topRankedLabels: fakeTopRankedLabels } });
});
it('provides all the labels to the labels selector deduplicated', () => {
const wrapperLabelIds = labelIds(fakeTopRankedLabels);
const result = [...labelIds(groupLabels), 1337];
expect(wrapperLabelIds).toEqual(result);
});
});
});
describe('with no data', () => {
......
......@@ -28,6 +28,7 @@ describe('ValueStreamForm', () => {
const createValueStreamMock = jest.fn(() => Promise.resolve());
const updateValueStreamMock = jest.fn(() => Promise.resolve());
const fetchGroupLabelsMock = jest.fn(() => Promise.resolve());
const mockEvent = { preventDefault: jest.fn() };
const mockToastShow = jest.fn();
const streamName = 'Cool stream';
......@@ -49,23 +50,26 @@ describe('ValueStreamForm', () => {
const initialPreset = PRESET_OPTIONS_DEFAULT;
const fakeStore = () =>
const fakeStore = ({ state }) =>
new Vuex.Store({
state: {
isCreatingValueStream: false,
formEvents,
defaultGroupLabels: null,
...state,
},
actions: {
createValueStream: createValueStreamMock,
updateValueStream: updateValueStreamMock,
fetchGroupLabels: fetchGroupLabelsMock,
},
});
const createComponent = ({ props = {}, data = {}, stubs = {} } = {}) =>
const createComponent = ({ props = {}, data = {}, stubs = {}, state = {} } = {}) =>
extendedWrapper(
shallowMount(ValueStreamForm, {
localVue,
store: fakeStore(),
store: fakeStore({ state }),
data() {
return {
...data,
......@@ -140,6 +144,10 @@ describe('ValueStreamForm', () => {
expect(findHiddenStages().length).toBe(0);
});
it('will fetch group labels', () => {
expect(fetchGroupLabelsMock).toHaveBeenCalled();
});
describe('Add stage button', () => {
beforeEach(() => {
wrapper = createComponent({
......@@ -383,6 +391,18 @@ describe('ValueStreamForm', () => {
});
});
describe('defaultGroupLabels set', () => {
beforeEach(() => {
wrapper = createComponent({
state: { defaultGroupLabels: [] },
});
});
it('does not fetch group labels', () => {
expect(fetchGroupLabelsMock).not.toHaveBeenCalled();
});
});
describe('form errors', () => {
beforeEach(() => {
wrapper = createComponent({
......
......@@ -7,7 +7,8 @@ import testAction from 'helpers/vuex_action_helper';
import { createdAfter, createdBefore, currentGroup } from 'jest/cycle_analytics/mock_data';
import { I18N_VSA_ERROR_STAGES, I18N_VSA_ERROR_STAGE_MEDIAN } from '~/cycle_analytics/constants';
import createFlash from '~/flash';
import { allowedStages as stages, valueStreams } from '../mock_data';
import httpStatusCodes from '~/lib/utils/http_status';
import { allowedStages as stages, valueStreams, endpoints, groupLabels } from '../mock_data';
const group = { fullPath: 'fake_group_full_path' };
const milestonesPath = 'fake_milestones_path';
......@@ -291,6 +292,7 @@ describe('Value Stream Analytics actions', () => {
${'typeOfWork/setLoading'} | ${true}
`('dispatches $action', async ({ action, args }) => {
await actions.initializeCycleAnalytics(store, initialData);
expect(mockDispatch).toHaveBeenCalledWith(action, args);
});
......@@ -351,4 +353,38 @@ describe('Value Stream Analytics actions', () => {
[],
));
});
describe('fetchGroupLabels', () => {
beforeEach(() => {
mock.onGet(endpoints.groupLabels).reply(httpStatusCodes.OK, groupLabels);
});
it(`will commit the "REQUEST_GROUP_LABELS" and "RECEIVE_GROUP_LABELS_SUCCESS" mutations`, () => {
return testAction({
action: actions.fetchGroupLabels,
state,
expectedMutations: [
{ type: types.REQUEST_GROUP_LABELS },
{ type: types.RECEIVE_GROUP_LABELS_SUCCESS, payload: groupLabels },
],
});
});
describe('with a failed request', () => {
beforeEach(() => {
mock.onGet(endpoints.groupLabels).reply(httpStatusCodes.BAD_REQUEST);
});
it(`will commit the "RECEIVE_GROUP_LABELS_ERROR" mutation`, () => {
return testAction({
action: actions.fetchGroupLabels,
state,
expectedMutations: [
{ type: types.REQUEST_GROUP_LABELS },
{ type: types.RECEIVE_GROUP_LABELS_ERROR },
],
});
});
});
});
});
......@@ -15,6 +15,7 @@ import {
valueStreams,
rawCustomStageEvents,
camelCasedStageEvents,
groupLabels,
} from '../mock_data';
let state = null;
......@@ -62,6 +63,8 @@ describe('Value Stream Analytics mutations', () => {
${types.INITIALIZE_VALUE_STREAM_SUCCESS} | ${'isLoading'} | ${false}
${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}}
${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}}
${types.REQUEST_GROUP_LABELS} | ${'defaultGroupLabels'} | ${[]}
${types.RECEIVE_GROUP_LABELS_ERROR} | ${'defaultGroupLabels'} | ${[]}
${types.SET_STAGE_EVENTS} | ${'formEvents'} | ${[]}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state);
......@@ -96,6 +99,7 @@ describe('Value Stream Analytics mutations', () => {
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }}
${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }}
${types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }}
${types.RECEIVE_GROUP_LABELS_SUCCESS} | ${groupLabels} | ${{ defaultGroupLabels: groupLabels }}
${types.SET_PAGINATION} | ${pagination} | ${{ pagination: { ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC } }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${{ pagination: { ...pagination, sort: 'duration', direction: 'asc' } }}
${types.SET_STAGE_EVENTS} | ${rawCustomStageEvents} | ${{ formEvents: camelCasedStageEvents }}
......
# frozen_string_literal: true
require "spec_helper"
RSpec.describe EE::Groups::Analytics::CycleAnalyticsHelper do
describe '#group_cycle_analytics_data' do
let(:image_path_keys) { [:empty_state_svg_path, :no_data_svg_path, :no_access_svg_path] }
let(:additional_data_keys) { [:default_stages] }
subject(:group_cycle_analytics) { helper.group_cycle_analytics_data(group) }
context 'when a group is present' do
let(:group) { create(:group) }
let(:api_path_keys) { [:milestones_path, :labels_path] }
it "sets the correct data keys" do
expect(group_cycle_analytics.keys)
.to match_array(api_path_keys + image_path_keys + additional_data_keys)
end
end
context 'when a group is not present' do
let(:group) { nil }
it "sets the correct data keys" do
expect(group_cycle_analytics.keys)
.to match_array(image_path_keys + additional_data_keys)
end
end
end
end
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