Commit d90ee4bb authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Doug Stull

Store fetched group labels in vuex

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