Commit c1a54dbe authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ek-fix-vsa-label-dropdown-limit' into 'master'

Fix vsa label dropdown limit

Closes #212895

See merge request gitlab-org/gitlab!28073
parents c39941b0 5f6492f7
...@@ -65,7 +65,6 @@ export default { ...@@ -65,7 +65,6 @@ export default {
'selectedStage', 'selectedStage',
'stages', 'stages',
'summary', 'summary',
'labels',
'topRankedLabels', 'topRankedLabels',
'currentStageEvents', 'currentStageEvents',
'customStageFormEvents', 'customStageFormEvents',
...@@ -302,7 +301,6 @@ export default { ...@@ -302,7 +301,6 @@ export default {
:current-stage-events="currentStageEvents" :current-stage-events="currentStageEvents"
:custom-stage-form-events="customStageFormEvents" :custom-stage-form-events="customStageFormEvents"
:custom-stage-form-errors="customStageFormErrors" :custom-stage-form-errors="customStageFormErrors"
:labels="labels"
:no-data-svg-path="noDataSvgPath" :no-data-svg-path="noDataSvgPath"
:no-access-svg-path="noAccessSvgPath" :no-access-svg-path="noAccessSvgPath"
:can-edit-stages="hasCustomizableCycleAnalytics" :can-edit-stages="hasCustomizableCycleAnalytics"
...@@ -350,7 +348,6 @@ export default { ...@@ -350,7 +348,6 @@ export default {
<tasks-by-type-chart <tasks-by-type-chart
:chart-data="tasksByTypeChartData" :chart-data="tasksByTypeChartData"
:filters="selectedTasksByTypeFilters" :filters="selectedTasksByTypeFilters"
:labels="labels"
@updateFilter="setTasksByTypeFilters" @updateFilter="setTasksByTypeFilters"
/> />
</div> </div>
......
...@@ -72,10 +72,6 @@ export default { ...@@ -72,10 +72,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
labels: {
type: Array,
required: true,
},
initialFields: { initialFields: {
type: Object, type: Object,
required: false, required: false,
...@@ -326,8 +322,7 @@ export default { ...@@ -326,8 +322,7 @@ export default {
:invalid-feedback="fieldErrorMessage('startEventLabelId')" :invalid-feedback="fieldErrorMessage('startEventLabelId')"
> >
<labels-selector <labels-selector
:labels="labels" :selected-label-id="[fields.startEventLabelId]"
:selected-label-id="fields.startEventLabelId"
name="custom-stage-start-event-label" name="custom-stage-start-event-label"
@selectLabel="handleSelectLabel('startEventLabelId', $event)" @selectLabel="handleSelectLabel('startEventLabelId', $event)"
@clearLabel="handleClearLabel('startEventLabelId')" @clearLabel="handleClearLabel('startEventLabelId')"
...@@ -363,8 +358,7 @@ export default { ...@@ -363,8 +358,7 @@ export default {
:invalid-feedback="fieldErrorMessage('endEventLabelId')" :invalid-feedback="fieldErrorMessage('endEventLabelId')"
> >
<labels-selector <labels-selector
:labels="labels" :selected-label-id="[fields.endEventLabelId]"
:selected-label-id="fields.endEventLabelId"
name="custom-stage-stop-event-label" name="custom-stage-stop-event-label"
@selectLabel="handleSelectLabel('endEventLabelId', $event)" @selectLabel="handleSelectLabel('endEventLabelId', $event)"
@clearLabel="handleClearLabel('endEventLabelId')" @clearLabel="handleClearLabel('endEventLabelId')"
......
<script> <script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import Api from 'ee/api';
import { debounce } from 'lodash';
import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { removeFlash } from '../utils';
const DATA_REFETCH_DELAY = 250;
export default { export default {
name: 'LabelsSelector', name: 'LabelsSelector',
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlIcon,
GlLoadingIcon,
GlSearchBoxByType,
}, },
props: { props: {
labels: { defaultSelectedLabelIds: {
type: Array, type: Array,
required: true, required: false,
default: () => [],
}, },
selectedLabelId: { maxLabels: {
type: Number, type: Number,
required: false, required: false,
default: null, default: 0,
},
multiselect: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
selectedLabelId: {
type: Array,
required: false,
default: () => [],
},
right: {
type: Boolean,
required: false,
default: false,
},
dropdownItemClass: {
type: String,
required: false,
default: '',
}, },
}, },
data() {
return {
loading: false,
searchTerm: '',
labels: [],
selectedLabelIds: this.defaultSelectedLabelIds || [],
};
},
computed: { computed: {
selectedLabel() { selectedLabel() {
const { selectedLabelId, labels } = this; const { selectedLabelId, labels = [] } = this;
if (!selectedLabelId || !labels.length) return null; if (!selectedLabelId.length || !labels.length) return null;
return labels.find(({ id }) => id === selectedLabelId); return labels.find(({ id }) => selectedLabelId.includes(id));
},
maxLabelsSelected() {
return this.selectedLabelIds.length >= this.maxLabels;
},
noMatchingLabels() {
return Boolean(this.searchTerm.length && !this.labels.length);
},
},
watch: {
searchTerm() {
debounce(this.fetchData(), DATA_REFETCH_DELAY);
}, },
}, },
mounted() {
this.fetchData();
},
methods: { methods: {
...mapGetters(['currentGroupPath']),
fetchData() {
removeFlash();
this.loading = true;
return Api.cycleAnalyticsGroupLabels(this.currentGroupPath, {
search: this.searchTerm,
only_group_labels: true,
})
.then(({ data }) => {
this.labels = data;
})
.catch(() => {
createFlash(__('There was an error fetching label data for the selected group'));
})
.finally(() => {
this.loading = false;
});
},
labelTitle(label) { labelTitle(label) {
// there are 2 possible endpoints for group labels // there are 2 possible endpoints for group labels
// one returns label.name the other label.title // one returns label.name the other label.title
return label?.name || label.title; return label?.name || label.title;
}, },
isSelectedLabel(id) { isSelectedLabel(id) {
return this.selectedLabelId && id === this.selectedLabelId; return Boolean(this.selectedLabelId?.includes(id));
},
isDisabledLabel(id) {
return Boolean(this.maxLabelsSelected && !this.isSelectedLabel(id));
}, },
}, },
}; };
</script> </script>
<template> <template>
<gl-dropdown class="w-100" toggle-class="overflow-hidden"> <gl-dropdown class="w-100" toggle-class="overflow-hidden" :right="right">
<template slot="button-content"> <template #button-content>
<span v-if="selectedLabel"> <slot name="label-dropdown-button">
<span <span v-if="selectedLabel">
:style="{ backgroundColor: selectedLabel.color }" <span
class="d-inline-block dropdown-label-box" :style="{ backgroundColor: selectedLabel.color }"
> class="d-inline-block dropdown-label-box"
>
</span>
{{ labelTitle(selectedLabel) }}
</span> </span>
{{ labelTitle(selectedLabel) }} <span v-else>{{ __('Select a label') }}</span>
</span> </slot>
<span v-else>{{ __('Select a label') }}</span> </template>
<template>
<slot name="label-dropdown-list-header">
<gl-dropdown-item :active="!selectedLabelId.length" @click.prevent="$emit('clearLabel')"
>{{ __('Select a label') }}
</gl-dropdown-item>
</slot>
<div class="mb-3 px-3">
<gl-search-box-by-type v-model.trim="searchTerm" class="mb-2" />
</div>
<div class="mb-3 px-3">
<gl-dropdown-item
v-for="label in labels"
:key="label.id"
:class="{
'pl-4': multiselect && !isSelectedLabel(label.id),
'cursor-not-allowed': disabled,
}"
:active="isSelectedLabel(label.id)"
@click.prevent="$emit('selectLabel', label.id, selectedLabelIds)"
>
<gl-icon
v-if="multiselect && isSelectedLabel(label.id)"
class="text-gray-700 mr-1 vertical-align-middle"
name="mobile-issue-close"
/>
<span :style="{ backgroundColor: label.color }" class="d-inline-block dropdown-label-box">
</span>
{{ labelTitle(label) }}
</gl-dropdown-item>
<div v-show="loading" class="text-center">
<gl-loading-icon :inline="true" size="md" />
</div>
<div v-show="noMatchingLabels" class="text-secondary">
{{ __('No matching labels') }}
</div>
</div>
</template> </template>
<gl-dropdown-item :active="!selectedLabelId" @click.prevent="$emit('clearLabel')"
>{{ __('Select a label') }}
</gl-dropdown-item>
<gl-dropdown-item
v-for="label in labels"
:key="label.id"
:active="isSelectedLabel(label.id)"
@click.prevent="$emit('selectLabel', label.id)"
>
<span :style="{ backgroundColor: label.color }" class="d-inline-block dropdown-label-box">
</span>
{{ labelTitle(label) }}
</gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>
...@@ -72,10 +72,6 @@ export default { ...@@ -72,10 +72,6 @@ export default {
required: false, required: false,
default: () => {}, default: () => {},
}, },
labels: {
type: Array,
required: true,
},
noDataSvgPath: { noDataSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -230,7 +226,6 @@ export default { ...@@ -230,7 +226,6 @@ export default {
<custom-stage-form <custom-stage-form
v-else-if="isCreatingCustomStage || isEditingCustomStage" v-else-if="isCreatingCustomStage || isEditingCustomStage"
:events="customStageFormEvents" :events="customStageFormEvents"
:labels="labels"
:is-saving-custom-stage="isSavingCustomStage" :is-saving-custom-stage="isSavingCustomStage"
:initial-fields="customStageFormInitialData" :initial-fields="customStageFormInitialData"
:is-editing-custom-stage="isEditingCustomStage" :is-editing-custom-stage="isEditingCustomStage"
......
...@@ -20,10 +20,6 @@ export default { ...@@ -20,10 +20,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
labels: {
type: Array,
required: true,
},
}, },
computed: { computed: {
hasData() { hasData() {
...@@ -69,7 +65,6 @@ export default { ...@@ -69,7 +65,6 @@ export default {
<div v-if="hasData"> <div v-if="hasData">
<p>{{ summaryDescription }}</p> <p>{{ summaryDescription }}</p>
<tasks-by-type-filters <tasks-by-type-filters
:labels="labels"
:selected-label-ids="filters.selectedLabelIds" :selected-label-ids="filters.selectedLabelIds"
:subject-filter="selectedSubjectFilter" :subject-filter="selectedSubjectFilter"
@updateFilter="$emit('updateFilter', $event)" @updateFilter="$emit('updateFilter', $event)"
......
<script> <script>
import { import { GlDropdownDivider, GlSegmentedControl, GlIcon } from '@gitlab/ui';
GlDropdownDivider,
GlSegmentedControl,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { removeFlash } from '../utils'; import { removeFlash } from '../utils';
...@@ -16,6 +9,7 @@ import { ...@@ -16,6 +9,7 @@ import {
TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS, TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS,
TASKS_BY_TYPE_MAX_LABELS, TASKS_BY_TYPE_MAX_LABELS,
} from '../constants'; } from '../constants';
import LabelsSelector from './labels_selector.vue';
export default { export default {
name: 'TasksByTypeFilters', name: 'TasksByTypeFilters',
...@@ -23,37 +17,23 @@ export default { ...@@ -23,37 +17,23 @@ export default {
GlSegmentedControl, GlSegmentedControl,
GlDropdownDivider, GlDropdownDivider,
GlIcon, GlIcon,
GlDropdown, LabelsSelector,
GlDropdownItem,
GlSearchBoxByType,
}, },
props: { props: {
maxLabels: { selectedLabelIds: {
type: Number,
required: false,
default: TASKS_BY_TYPE_MAX_LABELS,
},
labels: {
type: Array, type: Array,
required: true, required: true,
}, },
selectedLabelIds: { maxLabels: {
type: Array, type: Number,
required: false, required: false,
default: () => [], default: TASKS_BY_TYPE_MAX_LABELS,
}, },
subjectFilter: { subjectFilter: {
type: String, type: String,
required: true, required: true,
}, },
}, },
data() {
const { subjectFilter: selectedSubjectFilter } = this;
return {
selectedSubjectFilter,
labelsSearchTerm: '',
};
},
computed: { computed: {
subjectFilterOptions() { subjectFilterOptions() {
return Object.entries(TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS).map(([value, text]) => ({ return Object.entries(TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS).map(([value, text]) => ({
...@@ -63,10 +43,9 @@ export default { ...@@ -63,10 +43,9 @@ export default {
}, },
selectedFiltersText() { selectedFiltersText() {
const { subjectFilter, selectedLabelIds } = this; const { subjectFilter, selectedLabelIds } = this;
const subjectFilterText = const subjectFilterText = TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[subjectFilter]
subjectFilter === TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[subjectFilter] ? TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[subjectFilter]
? TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[subjectFilter] : TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[TASKS_BY_TYPE_SUBJECT_ISSUE];
: TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS[TASKS_BY_TYPE_SUBJECT_ISSUE];
return sprintf( return sprintf(
s__('CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCount} labels'), s__('CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCount} labels'),
{ {
...@@ -75,11 +54,6 @@ export default { ...@@ -75,11 +54,6 @@ export default {
}, },
); );
}, },
availableLabels() {
return this.labels.filter(({ name }) =>
name.toLowerCase().includes(this.labelsSearchTerm.toLowerCase()),
);
},
selectedLabelLimitText() { selectedLabelLimitText() {
const { selectedLabelIds, maxLabels } = this; const { selectedLabelIds, maxLabels } = this;
return sprintf(s__('CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)'), { return sprintf(s__('CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)'), {
...@@ -90,21 +64,12 @@ export default { ...@@ -90,21 +64,12 @@ export default {
maxLabelsSelected() { maxLabelsSelected() {
return this.selectedLabelIds.length >= this.maxLabels; return this.selectedLabelIds.length >= this.maxLabels;
}, },
hasMatchingLabels() {
return this.availableLabels.length;
},
}, },
methods: { methods: {
canUpdateLabelFilters(value) { canUpdateLabelFilters(value) {
// we can always remove a filter // we can always remove a filter
return this.selectedLabelIds.includes(value) || !this.maxLabelsSelected; return this.selectedLabelIds.includes(value) || !this.maxLabelsSelected;
}, },
isLabelSelected(id) {
return this.selectedLabelIds.includes(id);
},
isLabelDisabled(id) {
return this.maxLabelsSelected && !this.isLabelSelected(id);
},
handleLabelSelected(value) { handleLabelSelected(value) {
removeFlash('notice'); removeFlash('notice');
if (this.canUpdateLabelFilters(value)) { if (this.canUpdateLabelFilters(value)) {
...@@ -132,59 +97,41 @@ export default { ...@@ -132,59 +97,41 @@ export default {
<p>{{ selectedFiltersText }}</p> <p>{{ selectedFiltersText }}</p>
</div> </div>
<div class="flex-column"> <div class="flex-column">
<gl-dropdown <labels-selector
aria-expanded="false" :default-selected-labels-ids="selectedLabelIds"
:max-labels="maxLabels"
:aria-label="__('CycleAnalytics|Display chart filters')" :aria-label="__('CycleAnalytics|Display chart filters')"
:selected-label-id="selectedLabelIds"
aria-expanded="false"
multiselect
right right
@selectLabel="handleLabelSelected"
> >
<template #button-content> <template #label-dropdown-button>
<gl-icon class="vertical-align-top" name="settings" /> <gl-icon class="vertical-align-top" name="settings" />
<gl-icon name="chevron-down" /> <gl-icon name="chevron-down" />
</template> </template>
<div class="mb-3 px-3"> <template #label-dropdown-list-header>
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p> <div class="mb-3 px-3">
<gl-segmented-control <p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
v-model="selectedSubjectFilter" <gl-segmented-control
:options="subjectFilterOptions" :checked="subjectFilter"
@input=" :options="subjectFilterOptions"
value => @input="
$emit('updateFilter', { filter: $options.TASKS_BY_TYPE_FILTERS.SUBJECT, value }) value =>
" $emit('updateFilter', { filter: $options.TASKS_BY_TYPE_FILTERS.SUBJECT, value })
/> "
</div>
<gl-dropdown-divider />
<div class="mb-3 px-3">
<p class="font-weight-bold text-left my-2">
{{ s__('CycleAnalytics|Select labels') }}
<br /><small>{{ selectedLabelLimitText }}</small>
</p>
<gl-search-box-by-type v-model.trim="labelsSearchTerm" class="mb-2" />
<gl-dropdown-item
v-for="label in availableLabels"
:key="label.id"
:disabled="isLabelDisabled(label.id)"
:class="{
'pl-4': !isLabelSelected(label.id),
'cursor-not-allowed': isLabelDisabled(label.id),
}"
@click="() => handleLabelSelected(label.id)"
>
<gl-icon
v-if="isLabelSelected(label.id)"
class="text-gray-700 mr-1 vertical-align-middle"
name="mobile-issue-close"
/> />
<span
:style="{ 'background-color': label.color }"
class="d-inline-block dropdown-label-box"
></span>
{{ label.name }}
</gl-dropdown-item>
<div v-show="!hasMatchingLabels" class="text-secondary">
{{ __('No matching labels') }}
</div> </div>
</div> <gl-dropdown-divider />
</gl-dropdown> <div class="mb-3 px-3">
<p class="font-weight-bold text-left my-2">
{{ s__('CycleAnalytics|Select labels') }}
<br /><small>{{ selectedLabelLimitText }}</small>
</p>
</div>
</template>
</labels-selector>
</div> </div>
</div> </div>
</template> </template>
...@@ -132,7 +132,6 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => { ...@@ -132,7 +132,6 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
dispatch('requestCycleAnalyticsData'); dispatch('requestCycleAnalyticsData');
return Promise.resolve() return Promise.resolve()
.then(() => dispatch('fetchGroupLabels'))
.then(() => dispatch('fetchGroupStagesAndEvents')) .then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues')) .then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('fetchSummaryData')) .then(() => dispatch('fetchSummaryData'))
...@@ -207,29 +206,6 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => { ...@@ -207,29 +206,6 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => {
export const requestGroupStagesAndEvents = ({ commit }) => export const requestGroupStagesAndEvents = ({ commit }) =>
commit(types.REQUEST_GROUP_STAGES_AND_EVENTS); commit(types.REQUEST_GROUP_STAGES_AND_EVENTS);
export const receiveGroupLabelsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_GROUP_LABELS_SUCCESS, data);
export const receiveGroupLabelsError = ({ commit }, error) => {
commit(types.RECEIVE_GROUP_LABELS_ERROR, error);
createFlash(__('There was an error fetching label data for the selected group'));
};
export const requestGroupLabels = ({ commit }) => commit(types.REQUEST_GROUP_LABELS);
export const fetchGroupLabels = ({ dispatch, state }) => {
dispatch('requestGroupLabels');
const {
selectedGroup: { fullPath, parentId = null },
} = state;
return Api.cycleAnalyticsGroupLabels(parentId || fullPath)
.then(({ data }) => dispatch('receiveGroupLabelsSuccess', data))
.catch(error =>
handleErrorOrRethrow({ error, action: () => dispatch('receiveGroupLabelsError', error) }),
);
};
export const receiveTopRankedGroupLabelsSuccess = ({ commit, dispatch }, data) => { export const receiveTopRankedGroupLabelsSuccess = ({ commit, dispatch }, data) => {
commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS, data); commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS, data);
dispatch('fetchTasksByTypeData'); dispatch('fetchTasksByTypeData');
......
...@@ -24,10 +24,6 @@ export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM'; ...@@ -24,10 +24,6 @@ export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM';
export const SHOW_EDIT_CUSTOM_STAGE_FORM = 'SHOW_EDIT_CUSTOM_STAGE_FORM'; export const SHOW_EDIT_CUSTOM_STAGE_FORM = 'SHOW_EDIT_CUSTOM_STAGE_FORM';
export const CLEAR_CUSTOM_STAGE_FORM_ERRORS = 'CLEAR_CUSTOM_STAGE_FORM_ERRORS'; export const CLEAR_CUSTOM_STAGE_FORM_ERRORS = 'CLEAR_CUSTOM_STAGE_FORM_ERRORS';
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 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_SUCCESS = 'RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS';
export const RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR = 'RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR'; export const RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR = 'RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR';
......
...@@ -56,15 +56,6 @@ export default { ...@@ -56,15 +56,6 @@ export default {
state.isEmptyStage = true; state.isEmptyStage = true;
state.isLoadingStage = false; state.isLoadingStage = false;
}, },
[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) { [types.REQUEST_TOP_RANKED_GROUP_LABELS](state) {
state.topRankedLabels = []; state.topRankedLabels = [];
state.tasksByType = { state.tasksByType = {
......
...@@ -29,7 +29,6 @@ export default () => ({ ...@@ -29,7 +29,6 @@ export default () => ({
stages: [], stages: [],
summary: [], summary: [],
labels: [],
topRankedLabels: [], topRankedLabels: [],
medians: {}, medians: {},
......
...@@ -22,7 +22,7 @@ export default { ...@@ -22,7 +22,7 @@ export default {
cycleAnalyticsStagePath: '/-/analytics/value_stream_analytics/stages/:stage_id', cycleAnalyticsStagePath: '/-/analytics/value_stream_analytics/stages/:stage_id',
cycleAnalyticsDurationChartPath: cycleAnalyticsDurationChartPath:
'/-/analytics/value_stream_analytics/stages/:stage_id/duration_chart', '/-/analytics/value_stream_analytics/stages/:stage_id/duration_chart',
cycleAnalyticsGroupLabelsPath: '/api/:version/groups/:namespace_path/labels', cycleAnalyticsGroupLabelsPath: '/groups/:namespace_path/-/labels.json',
codeReviewAnalyticsPath: '/api/:version/analytics/code_review', codeReviewAnalyticsPath: '/api/:version/analytics/code_review',
groupActivityIssuesPath: '/api/:version/analytics/group_activity/issues_count', groupActivityIssuesPath: '/api/:version/analytics/group_activity/issues_count',
groupActivityMergeRequestsPath: '/api/:version/analytics/group_activity/merge_requests_count', groupActivityMergeRequestsPath: '/api/:version/analytics/group_activity/merge_requests_count',
...@@ -200,7 +200,7 @@ export default { ...@@ -200,7 +200,7 @@ export default {
}); });
}, },
cycleAnalyticsGroupLabels(groupId, params = {}) { cycleAnalyticsGroupLabels(groupId, params = { search: null }) {
// TODO: This can be removed when we resolve the labels endpoint // TODO: This can be removed when we resolve the labels endpoint
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25746 // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25746
const url = Api.buildUrl(this.cycleAnalyticsGroupLabelsPath).replace( const url = Api.buildUrl(this.cycleAnalyticsGroupLabelsPath).replace(
......
---
title: Fix vsa label dropdown limit
merge_request: 28073
author:
type: fixed
...@@ -4,14 +4,16 @@ require 'spec_helper' ...@@ -4,14 +4,16 @@ require 'spec_helper'
describe 'Group Value Stream Analytics', :js do describe 'Group Value Stream Analytics', :js do
include DragTo include DragTo
let!(:user) { create(:user) } let_it_be(:user) { create(:user) }
let!(:group) { create(:group, name: "CA-test-group") } let_it_be(:group) { create(:group, name: "CA-test-group") }
let!(:sub_group) { create(:group, name: "CA-sub-group", parent: group) } let_it_be(:sub_group) { create(:group, name: "CA-sub-group", parent: group) }
let!(:group2) { create(:group, name: "CA-bad-test-group") } let_it_be(:group2) { create(:group, name: "CA-bad-test-group") }
let!(:project) { create(:project, :repository, namespace: group, group: group, name: "Cool fun project") } let_it_be(:project) { create(:project, :repository, namespace: group, group: group, name: "Cool fun project") }
let!(:label) { create(:group_label, group: group) } let_it_be(:group_label1) { create(:group_label, group: group) }
let!(:label2) { create(:group_label, group: group) } let_it_be(:group_label2) { create(:group_label, group: group) }
let!(:label3) { create(:group_label, group: group2) } let_it_be(:label) { create(:group_label, group: group2) }
let_it_be(:sub_group_label1) { create(:group_label, group: sub_group) }
let_it_be(:sub_group_label2) { create(:group_label, group: sub_group) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
...@@ -20,7 +22,7 @@ describe 'Group Value Stream Analytics', :js do ...@@ -20,7 +22,7 @@ describe 'Group Value Stream Analytics', :js do
stage_nav_selector = '.stage-nav' stage_nav_selector = '.stage-nav'
3.times do |i| 3.times do |i|
let!("issue_#{i}".to_sym) { create(:issue, title: "New Issue #{i}", project: project, created_at: 2.days.ago) } let_it_be("issue_#{i}".to_sym) { create(:issue, title: "New Issue #{i}", project: project, created_at: 2.days.ago) }
end end
shared_examples 'empty state' do shared_examples 'empty state' do
...@@ -268,7 +270,7 @@ describe 'Group Value Stream Analytics', :js do ...@@ -268,7 +270,7 @@ describe 'Group Value Stream Analytics', :js do
end end
context 'with lots of data', :js do context 'with lots of data', :js do
let!(:issue) { create(:issue, project: project, created_at: 5.days.ago) } let_it_be(:issue) { create(:issue, project: project, created_at: 5.days.ago) }
around do |example| around do |example|
Timecop.freeze { example.run } Timecop.freeze { example.run }
...@@ -350,8 +352,8 @@ describe 'Group Value Stream Analytics', :js do ...@@ -350,8 +352,8 @@ describe 'Group Value Stream Analytics', :js do
context 'with data available' do context 'with data available' do
before do before do
3.times do |i| 3.times do |i|
create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [label]) create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [group_label1])
create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [label2]) create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [group_label2])
end end
visit analytics_cycle_analytics_path visit analytics_cycle_analytics_path
...@@ -436,8 +438,17 @@ describe 'Group Value Stream Analytics', :js do ...@@ -436,8 +438,17 @@ describe 'Group Value Stream Analytics', :js do
page.find("select[name='#{name}']").find("#{elem}[value=#{value}]").select_option page.find("select[name='#{name}']").find("#{elem}[value=#{value}]").select_option
end end
def wait_for_labels(field)
page.within("[name=#{field}]") do
find('.dropdown-toggle').click
wait_for_requests
expect(find('.dropdown-menu')).to have_selector('.dropdown-item')
end
end
def select_dropdown_label(field, index = 2) def select_dropdown_label(field, index = 2)
page.find("[name=#{field}] .dropdown-toggle").click
page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[index].click page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[index].click
end end
...@@ -626,24 +637,30 @@ describe 'Group Value Stream Analytics', :js do ...@@ -626,24 +637,30 @@ describe 'Group Value Stream Analytics', :js do
expect(page).to have_button('Add stage', disabled: true) expect(page).to have_button('Add stage', disabled: true)
end end
it 'does not contain labels from outside the group' do context 'with labels available' do
field = 'custom-stage-start-event-label' start_field = "custom-stage-start-event-label"
page.find("[name=#{field}] .dropdown-toggle").click end_field = "custom-stage-stop-event-label"
menu = page.find("[name=#{field}] .dropdown-menu")
expect(menu).not_to have_content(label3.name) it 'does not contain labels from outside the group' do
expect(menu).to have_content(label.name) wait_for_labels(start_field)
expect(menu).to have_content(label2.name) menu = page.find("[name=#{start_field}] .dropdown-menu")
end
context 'with all required fields set' do expect(menu).not_to have_content(other_label.name)
before do expect(menu).to have_content(first_label.name)
select_dropdown_label 'custom-stage-start-event-label', 1 expect(menu).to have_content(second_label.name)
select_dropdown_label 'custom-stage-stop-event-label', 2
end end
it_behaves_like 'submits the form successfully', custom_stage_with_labels_name context 'with all required fields set' do
before do
wait_for_labels(start_field)
select_dropdown_label start_field, 1
wait_for_labels(end_field)
select_dropdown_label end_field, 2
end
it_behaves_like 'submits the form successfully', custom_stage_with_labels_name
end
end end
end end
end end
...@@ -725,7 +742,11 @@ describe 'Group Value Stream Analytics', :js do ...@@ -725,7 +742,11 @@ describe 'Group Value Stream Analytics', :js do
select_group select_group
end end
it_behaves_like 'can create custom stages' it_behaves_like 'can create custom stages' do
let(:first_label) { group_label1 }
let(:second_label) { group_label2 }
let(:other_label) { label }
end
end end
context 'with a custom stage created' do context 'with a custom stage created' do
...@@ -746,7 +767,11 @@ describe 'Group Value Stream Analytics', :js do ...@@ -746,7 +767,11 @@ describe 'Group Value Stream Analytics', :js do
select_group(sub_group.full_name) select_group(sub_group.full_name)
end end
it_behaves_like 'can create custom stages' it_behaves_like 'can create custom stages' do
let(:first_label) { sub_group_label1 }
let(:second_label) { sub_group_label2 }
let(:other_label) { label }
end
end end
context 'with a custom stage created' do context 'with a custom stage created' do
......
...@@ -6,52 +6,6 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display ...@@ -6,52 +6,6 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
</button>" </button>"
`; `;
exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__277\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_closed\\">Issue closed</option>
<option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_added_to_board\\">Issue first added to a board</option>
<option value=\\"issue_first_associated_with_milestone\\">Issue first associated with a milestone</option>
<option value=\\"plan_stage_start\\">Issue first associated with a milestone or issue first added to a board</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
<option value=\\"code_stage_start\\">Issue first mentioned in a commit</option>
<option value=\\"issue_label_added\\">Issue label was added</option>
<option value=\\"issue_label_removed\\">Issue label was removed</option>
<option value=\\"merge_request_closed\\">Merge request closed</option>
<option value=\\"merge_request_created\\">Merge request created</option>
<option value=\\"merge_request_first_deployed_to_production\\">Merge request first deployed to production</option>
<option value=\\"merge_request_label_added\\">Merge request label was added</option>
<option value=\\"merge_request_label_removed\\">Merge request label was removed</option>
<option value=\\"merge_request_last_build_finished\\">Merge request last build finish time</option>
<option value=\\"merge_request_last_build_started\\">Merge request last build start time</option>
<option value=\\"merge_request_merged\\">Merge request merged</option>
</select>"
`;
exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__237\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_closed\\">Issue closed</option>
<option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_added_to_board\\">Issue first added to a board</option>
<option value=\\"issue_first_associated_with_milestone\\">Issue first associated with a milestone</option>
<option value=\\"plan_stage_start\\">Issue first associated with a milestone or issue first added to a board</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
<option value=\\"code_stage_start\\">Issue first mentioned in a commit</option>
<option value=\\"issue_label_added\\">Issue label was added</option>
<option value=\\"issue_label_removed\\">Issue label was removed</option>
<option value=\\"merge_request_closed\\">Merge request closed</option>
<option value=\\"merge_request_created\\">Merge request created</option>
<option value=\\"merge_request_first_deployed_to_production\\">Merge request first deployed to production</option>
<option value=\\"merge_request_label_added\\">Merge request label was added</option>
<option value=\\"merge_request_label_removed\\">Merge request label was removed</option>
<option value=\\"merge_request_last_build_finished\\">Merge request last build finish time</option>
<option value=\\"merge_request_last_build_started\\">Merge request last build start time</option>
<option value=\\"merge_request_merged\\">Merge request merged</option>
</select>"
`;
exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = ` exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = `
"<button disabled=\\"disabled\\" type=\\"button\\" class=\\"js-save-stage btn btn-success\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" aria-hidden=\\"true\\" class=\\"align-text-bottom gl-spinner gl-spinner-orange gl-spinner-sm\\"></span></span> "<button disabled=\\"disabled\\" type=\\"button\\" class=\\"js-save-stage btn btn-success\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" aria-hidden=\\"true\\" class=\\"align-text-bottom gl-spinner gl-spinner-orange gl-spinner-sm\\"></span></span>
Add stage Add stage
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Value Stream Analytics LabelsSelector with no item selected will render the label selector 1`] = ` exports[`Value Stream Analytics LabelsSelector with no item selected will render the label selector 1`] = `
"<gl-dropdown-stub text=\\"\\" toggle-class=\\"overflow-hidden\\" class=\\"w-100\\"><template></template> "<gl-dropdown-stub text=\\"\\" toggle-class=\\"overflow-hidden\\" class=\\"w-100\\">
<gl-dropdown-item-stub active=\\"true\\">Select a label <gl-dropdown-item-stub active=\\"true\\">Select a label
</gl-dropdown-item-stub> </gl-dropdown-item-stub>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span> <div class=\\"mb-3 px-3\\">
roses <gl-search-box-by-type-stub value=\\"\\" clearbuttontitle=\\"Clear\\" class=\\"mb-2\\"></gl-search-box-by-type-stub>
</gl-dropdown-item-stub> </div>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span> <div class=\\"mb-3 px-3\\">
some space <gl-dropdown-item-stub class=\\"\\">
</gl-dropdown-item-stub> <!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span> roses
violets </gl-dropdown-item-stub>
</gl-dropdown-item-stub> <gl-dropdown-item-stub class=\\"\\">
<!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span>
some space
</gl-dropdown-item-stub>
<gl-dropdown-item-stub class=\\"\\">
<!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</gl-dropdown-item-stub>
<div class=\\"text-center\\" style=\\"display: none;\\">
<gl-loading-icon-stub label=\\"Loading\\" size=\\"md\\" color=\\"orange\\" inline=\\"true\\"></gl-loading-icon-stub>
</div>
<div class=\\"text-secondary\\" style=\\"display: none;\\">
No matching labels
</div>
</div>
</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 selectedLabelId set will render the label selector 1`] = `
"<gl-dropdown-stub text=\\"\\" toggle-class=\\"overflow-hidden\\" class=\\"w-100\\"><template></template> "<gl-dropdown-stub text=\\"\\" toggle-class=\\"overflow-hidden\\" class=\\"w-100\\">
<gl-dropdown-item-stub>Select a label <gl-dropdown-item-stub>Select a label
</gl-dropdown-item-stub> </gl-dropdown-item-stub>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span> <div class=\\"mb-3 px-3\\">
roses <gl-search-box-by-type-stub value=\\"\\" clearbuttontitle=\\"Clear\\" class=\\"mb-2\\"></gl-search-box-by-type-stub>
</gl-dropdown-item-stub> </div>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span> <div class=\\"mb-3 px-3\\">
some space <gl-dropdown-item-stub class=\\"\\">
</gl-dropdown-item-stub> <!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
<gl-dropdown-item-stub active=\\"true\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span> roses
violets </gl-dropdown-item-stub>
</gl-dropdown-item-stub> <gl-dropdown-item-stub class=\\"\\">
<!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 255, 255);\\"></span>
some space
</gl-dropdown-item-stub>
<gl-dropdown-item-stub active=\\"true\\" class=\\"\\">
<!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</gl-dropdown-item-stub>
<div class=\\"text-center\\" style=\\"display: none;\\">
<gl-loading-icon-stub label=\\"Loading\\" size=\\"md\\" color=\\"orange\\" inline=\\"true\\"></gl-loading-icon-stub>
</div>
<div class=\\"text-secondary\\" style=\\"display: none;\\">
No matching labels
</div>
</div>
</gl-dropdown-stub>" </gl-dropdown-stub>"
`; `;
...@@ -17,7 +17,7 @@ exports[`TasksByTypeChart with data available should render the loading chart 1` ...@@ -17,7 +17,7 @@ exports[`TasksByTypeChart with data available should render the loading chart 1`
<h3>Type of work</h3> <h3>Type of work</h3>
<div> <div>
<p>Showing data for group 'Gitlab Org' from Dec 11, 2019 to Jan 10, 2020</p> <p>Showing data for group 'Gitlab Org' from Dec 11, 2019 to Jan 10, 2020</p>
<tasks-by-type-filters-stub maxlabels=\\"15\\" labels=\\"[object Object],[object Object],[object Object]\\" selectedlabelids=\\"1,2,3\\" subjectfilter=\\"Issue\\"></tasks-by-type-filters-stub> <tasks-by-type-filters-stub selectedlabelids=\\"1,2,3\\" maxlabels=\\"15\\" subjectfilter=\\"Issue\\"></tasks-by-type-filters-stub>
<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> <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>
</div> </div>
</div> </div>
......
...@@ -38,6 +38,7 @@ const defaultStubs = { ...@@ -38,6 +38,7 @@ const defaultStubs = {
'stage-event-list': true, 'stage-event-list': true,
'stage-nav-item': true, 'stage-nav-item': true,
'tasks-by-type-chart': true, 'tasks-by-type-chart': true,
'labels-selector': true,
}; };
function createComponent({ function createComponent({
...@@ -383,7 +384,6 @@ describe('Cycle Analytics component', () => { ...@@ -383,7 +384,6 @@ describe('Cycle Analytics component', () => {
describe('with tasksByTypeChart=true', () => { describe('with tasksByTypeChart=true', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({ wrapper = createComponent({
shallow: false, shallow: false,
withStageSelected: true, withStageSelected: true,
...@@ -394,7 +394,6 @@ describe('Cycle Analytics component', () => { ...@@ -394,7 +394,6 @@ describe('Cycle Analytics component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore();
}); });
it('displays the tasks by type chart', () => { it('displays the tasks by type chart', () => {
...@@ -446,11 +445,6 @@ describe('Cycle Analytics component', () => { ...@@ -446,11 +445,6 @@ describe('Cycle Analytics component', () => {
endpoint: mockData.endpoints.baseStagesEndpoint, endpoint: mockData.endpoints.baseStagesEndpoint,
response: { ...mockData.customizableStagesAndEvents }, response: { ...mockData.customizableStagesAndEvents },
}, },
fetchGroupLabels: {
status: defaultStatus,
endpoint: mockData.endpoints.groupLabels,
response: [...mockData.groupLabels],
},
...overrides, ...overrides,
}; };
...@@ -524,24 +518,6 @@ describe('Cycle Analytics component', () => { ...@@ -524,24 +518,6 @@ describe('Cycle Analytics component', () => {
); );
}); });
it('will display an error if the fetchGroupLabels request fails', () => {
expect(findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({
overrides: {
fetchGroupLabels: {
endpoint: mockData.endpoints.groupLabels,
status: httpStatusCodes.NOT_FOUND,
response: { response: { status: httpStatusCodes.NOT_FOUND } },
},
},
});
return selectGroupAndFindError(
'There was an error fetching label data for the selected group',
);
});
it('will display an error if the fetchGroupStagesAndEvents request fails', () => { it('will display an error if the fetchGroupStagesAndEvents request fails', () => {
expect(findFlashError()).toBeNull(); expect(findFlashError()).toBeNull();
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import createStore from 'ee/analytics/cycle_analytics/store'; import createStore from 'ee/analytics/cycle_analytics/store';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import CustomStageForm, { import CustomStageForm, {
initializeFormData, initializeFormData,
} from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue'; } from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants'; import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants';
import { import {
endpoints,
groupLabels, groupLabels,
customStageEvents as events, customStageEvents as events,
labelStartEvent, labelStartEvent,
...@@ -31,6 +35,7 @@ const MERGE_REQUEST_CLOSED = 'merge_request_closed'; ...@@ -31,6 +35,7 @@ const MERGE_REQUEST_CLOSED = 'merge_request_closed';
let store = null; let store = null;
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
jest.mock('lodash/debounce', () => jest.fn);
describe('CustomStageForm', () => { describe('CustomStageForm', () => {
function createComponent(props = {}, stubs = {}) { function createComponent(props = {}, stubs = {}) {
...@@ -40,14 +45,18 @@ describe('CustomStageForm', () => { ...@@ -40,14 +45,18 @@ describe('CustomStageForm', () => {
store, store,
propsData: { propsData: {
events, events,
labels: groupLabels,
...props, ...props,
}, },
stubs, stubs: {
'labels-selector': false,
...stubs,
},
}); });
} }
let wrapper = null; let wrapper = null;
let mock;
const findEvent = ev => wrapper.emitted()[ev]; const findEvent = ev => wrapper.emitted()[ev];
const sel = { const sel = {
...@@ -104,12 +113,17 @@ describe('CustomStageForm', () => { ...@@ -104,12 +113,17 @@ describe('CustomStageForm', () => {
return _wrapper.vm.$nextTick(); return _wrapper.vm.$nextTick();
} }
const mockGroupLabelsRequest = () =>
new MockAdapter(axios).onGet(endpoints.groupLabels).reply(200, groupLabels);
beforeEach(() => { beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({}); wrapper = createComponent({});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore();
}); });
describe.each([ describe.each([
...@@ -170,14 +184,20 @@ describe('CustomStageForm', () => { ...@@ -170,14 +184,20 @@ describe('CustomStageForm', () => {
it('selects events with canBeStartEvent=true for the start events dropdown', () => { it('selects events with canBeStartEvent=true for the start events dropdown', () => {
const select = wrapper.find(sel.startEvent); const select = wrapper.find(sel.startEvent);
expect(select.html()).toMatchSnapshot();
events
.filter(ev => ev.canBeStartEvent)
.forEach(ev => {
expect(select.html()).toHaveHtml(
`<option value="${ev.identifier}">${ev.name}</option>`,
);
});
}); });
it('does not select events with canBeStartEvent=false for the start events dropdown', () => { it('does not select events with canBeStartEvent=false for the start events dropdown', () => {
const select = wrapper.find(sel.startEvent); const select = wrapper.find(sel.startEvent);
expect(select.html()).toMatchSnapshot();
stopEvents events
.filter(ev => !ev.canBeStartEvent) .filter(ev => !ev.canBeStartEvent)
.forEach(ev => { .forEach(ev => {
expect(select.html()).not.toHaveHtml( expect(select.html()).not.toHaveHtml(
...@@ -189,7 +209,10 @@ describe('CustomStageForm', () => { ...@@ -189,7 +209,10 @@ describe('CustomStageForm', () => {
describe('start event label', () => { describe('start event label', () => {
beforeEach(() => { beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent(); wrapper = createComponent();
return wrapper.vm.$nextTick();
}); });
afterEach(() => { afterEach(() => {
...@@ -217,14 +240,13 @@ describe('CustomStageForm', () => { ...@@ -217,14 +240,13 @@ describe('CustomStageForm', () => {
expect(wrapper.vm.fields.startEventLabelId).toEqual(null); expect(wrapper.vm.fields.startEventLabelId).toEqual(null);
wrapper.find(sel.startEvent).setValue(labelStartEvent.identifier); wrapper.find(sel.startEvent).setValue(labelStartEvent.identifier);
return Vue.nextTick() return waitForPromises()
.then(() => { .then(() => {
wrapper wrapper
.find(sel.startEventLabel) .find(sel.startEventLabel)
.findAll('.dropdown-item') .findAll('.dropdown-item')
.at(1) // item at index 0 is 'select a label' .at(1) // item at index 0 is 'select a label'
.trigger('click'); .trigger('click');
return Vue.nextTick(); return Vue.nextTick();
}) })
.then(() => { .then(() => {
...@@ -400,7 +422,7 @@ describe('CustomStageForm', () => { ...@@ -400,7 +422,7 @@ describe('CustomStageForm', () => {
}, },
}); });
return Vue.nextTick() return waitForPromises()
.then(() => { .then(() => {
wrapper wrapper
.find(sel.endEventLabel) .find(sel.endEventLabel)
......
import { mount, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import createStore from 'ee/analytics/cycle_analytics/store';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue'; import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue';
import { groupLabels } from '../mock_data'; import { groupLabels } from '../mock_data';
const selectedLabel = groupLabels[groupLabels.length - 1]; const selectedLabel = groupLabels[groupLabels.length - 1];
const findActiveItem = wrapper => const findActiveItem = wrapper =>
wrapper wrapper
.findAll('gl-dropdown-item-stub') .findAll('gl-dropdown-item-stub')
.filter(d => d.attributes('active')) .filter(d => d.attributes('active'))
.at(0); .at(0);
const findFlashError = () => document.querySelector('.flash-container .flash-text');
const mockGroupLabelsRequest = (status = 200) =>
new MockAdapter(axios).onGet().reply(status, groupLabels);
jest.mock('lodash/debounce', () => jest.fn);
describe('Value Stream Analytics LabelsSelector', () => { describe('Value Stream Analytics LabelsSelector', () => {
function createComponent({ props = {}, shallow = true } = {}) { let store = null;
const localVue = createLocalVue();
localVue.use(Vuex);
function createComponent({ props = { selectedLabelId: [] }, shallow = true } = {}) {
store = createStore();
const func = shallow ? shallowMount : mount; const func = shallow ? shallowMount : mount;
return func(LabelsSelector, { return func(LabelsSelector, {
localVue,
store: {
...store,
getters: {
...getters,
currentGroupPath: 'fake',
},
},
propsData: { propsData: {
labels: groupLabels, ...props,
selectedLabelId: props.selectedLabelId || null,
}, },
}); });
} }
let wrapper = null; let wrapper = null;
let mock = null;
const labelNames = groupLabels.map(({ name }) => name); const labelNames = groupLabels.map(({ name }) => name);
describe('with no item selected', () => { describe('with no item selected', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); mock = mockGroupLabelsRequest();
wrapper = createComponent({});
return waitForPromises();
}); });
afterEach(() => { afterEach(() => {
mock.restore();
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
...@@ -49,9 +78,27 @@ describe('Value Stream Analytics LabelsSelector', () => { ...@@ -49,9 +78,27 @@ describe('Value Stream Analytics LabelsSelector', () => {
expect(activeItem.text()).toEqual('Select a label'); expect(activeItem.text()).toEqual('Select a label');
}); });
describe('with a failed request', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest(404);
wrapper = createComponent({});
return waitForPromises();
});
it('should flash an error message', () => {
expect(findFlashError().innerText.trim()).toEqual(
'There was an error fetching label data for the selected group',
);
});
});
describe('when a dropdown item is clicked', () => { describe('when a dropdown item is clicked', () => {
beforeEach(() => { beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({ shallow: false }); wrapper = createComponent({ shallow: false });
return waitForPromises();
}); });
it('will emit the "selectLabel" event', () => { it('will emit the "selectLabel" event', () => {
...@@ -81,7 +128,9 @@ describe('Value Stream Analytics LabelsSelector', () => { ...@@ -81,7 +128,9 @@ describe('Value Stream Analytics LabelsSelector', () => {
describe('with selectedLabelId set', () => { describe('with selectedLabelId set', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ props: { selectedLabelId: selectedLabel.id } }); mock = mockGroupLabelsRequest();
wrapper = createComponent({ props: { selectedLabelId: [selectedLabel.id] } });
return waitForPromises();
}); });
afterEach(() => { afterEach(() => {
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue'; import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue';
import { TASKS_BY_TYPE_SUBJECT_ISSUE } from 'ee/analytics/cycle_analytics/constants'; import { TASKS_BY_TYPE_SUBJECT_ISSUE } from 'ee/analytics/cycle_analytics/constants';
import { groupLabels } from '../mock_data';
const seriesNames = ['Cool label', 'Normal label']; const seriesNames = ['Cool label', 'Normal label'];
const data = [[0, 1, 2], [5, 2, 3], [2, 4, 1]]; const data = [[0, 1, 2], [5, 2, 3], [2, 4, 1]];
...@@ -30,7 +29,6 @@ function createComponent({ props = {}, shallow = true, stubs = {} }) { ...@@ -30,7 +29,6 @@ function createComponent({ props = {}, shallow = true, stubs = {} }) {
data, data,
seriesNames, seriesNames,
}, },
labels: groupLabels,
...props, ...props,
}, },
stubs: { stubs: {
......
import { shallowMount, mount } from '@vue/test-utils'; import Vuex from 'vuex';
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 { GlDropdownItem, GlSegmentedControl } from '@gitlab/ui';
import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type_filters.vue'; import TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type_filters.vue';
import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue';
import { import {
TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_ISSUE,
TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST, TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
TASKS_BY_TYPE_FILTERS, TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants'; } from 'ee/analytics/cycle_analytics/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { shouldFlashAMessage } from '../helpers'; import { shouldFlashAMessage } from '../helpers';
import { groupLabels } from '../mock_data'; import { groupLabels } from '../mock_data';
import createStore from 'ee/analytics/cycle_analytics/store';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
const selectedLabelIds = [groupLabels[0].id]; const selectedLabelIds = [groupLabels[0].id];
const findSubjectFilters = ctx => ctx.find(GlSegmentedControl); const findSubjectFilters = ctx => ctx.find(GlSegmentedControl);
const findSelectedSubjectFilters = ctx => findSubjectFilters(ctx).attributes('checked'); const findSelectedSubjectFilters = ctx => findSubjectFilters(ctx).attributes('checked');
const findDropdownLabels = ctx => ctx.findAll(GlDropdownItem); const findDropdownLabels = ctx => ctx.find(LabelsSelector).findAll(GlDropdownItem);
const selectLabelAtIndex = (ctx, index) => { const selectLabelAtIndex = (ctx, index) => {
findDropdownLabels(ctx) findDropdownLabels(ctx)
.at(index) .at(index)
.vm.$emit('click'); .trigger('click');
return ctx.vm.$nextTick();
return waitForPromises();
}; };
const mockGroupLabelsRequest = () => new MockAdapter(axios).onGet().reply(200, groupLabels);
jest.mock('lodash/debounce', () => jest.fn);
let store = null;
const localVue = createLocalVue();
localVue.use(Vuex);
function createComponent({ props = {}, mountFn = shallowMount }) { function createComponent({ props = {}, mountFn = shallowMount }) {
store = createStore();
return mountFn(TasksByTypeFilters, { return mountFn(TasksByTypeFilters, {
localVue,
store: {
...store,
getters: {
...getters,
currentGroupPath: 'fake',
},
},
propsData: { propsData: {
selectedLabelIds, selectedLabelIds,
labels: groupLabels, labels: groupLabels,
...@@ -31,42 +55,50 @@ function createComponent({ props = {}, mountFn = shallowMount }) { ...@@ -31,42 +55,50 @@ function createComponent({ props = {}, mountFn = shallowMount }) {
...props, ...props,
}, },
stubs: { stubs: {
GlNewDropdown: true, LabelsSelector,
GlDropdownItem: true,
}, },
}); });
} }
describe('TasksByTypeFilters', () => { describe('TasksByTypeFilters', () => {
let wrapper = null; let wrapper = null;
let mock = null;
beforeEach(() => { beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({}); wrapper = createComponent({});
return waitForPromises();
}); });
afterEach(() => { afterEach(() => {
mock.restore();
wrapper.destroy(); wrapper.destroy();
}); });
describe('labels', () => { describe('labels', () => {
beforeEach(() => { beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({}); wrapper = createComponent({});
return waitForPromises();
}); });
it('emits the `updateFilter` event when a subject label is clicked', () => { it('emits the `updateFilter` event when a label is selected', () => {
expect(wrapper.emitted('updateFilter')).toBeUndefined(); expect(wrapper.emitted('updateFilter')).toBeUndefined();
return selectLabelAtIndex(wrapper, 0).then(() => {
expect(wrapper.emitted('updateFilter')).toBeDefined();
expect(wrapper.emitted('updateFilter')[0]).toEqual([ wrapper.find(LabelsSelector).vm.$emit('selectLabel', groupLabels[0].id);
{ filter: TASKS_BY_TYPE_FILTERS.LABEL, value: groupLabels[0].id },
]); expect(wrapper.emitted('updateFilter')).toBeDefined();
}); expect(wrapper.emitted('updateFilter')[0]).toEqual([
{ filter: TASKS_BY_TYPE_FILTERS.LABEL, value: groupLabels[0].id },
]);
}); });
describe('with the warningMessageThreshold label threshold reached', () => { describe('with the warningMessageThreshold label threshold reached', () => {
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest();
wrapper = createComponent({ wrapper = createComponent({
props: { props: {
maxLabels: 5, maxLabels: 5,
...@@ -75,7 +107,7 @@ describe('TasksByTypeFilters', () => { ...@@ -75,7 +107,7 @@ describe('TasksByTypeFilters', () => {
}, },
}); });
return selectLabelAtIndex(wrapper, 2); return waitForPromises().then(() => selectLabelAtIndex(wrapper, 2));
}); });
it('should indicate how many labels are selected', () => { it('should indicate how many labels are selected', () => {
...@@ -86,6 +118,8 @@ describe('TasksByTypeFilters', () => { ...@@ -86,6 +118,8 @@ describe('TasksByTypeFilters', () => {
describe('with maximum labels selected', () => { describe('with maximum labels selected', () => {
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest();
wrapper = createComponent({ wrapper = createComponent({
props: { props: {
maxLabels: 2, maxLabels: 2,
...@@ -94,7 +128,9 @@ describe('TasksByTypeFilters', () => { ...@@ -94,7 +128,9 @@ describe('TasksByTypeFilters', () => {
}, },
}); });
return selectLabelAtIndex(wrapper, 2); return waitForPromises().then(() => {
wrapper.find(LabelsSelector).vm.$emit('selectLabel', groupLabels[2].id);
});
}); });
it('should indicate how many labels are selected', () => { it('should indicate how many labels are selected', () => {
......
...@@ -18,7 +18,7 @@ const fixtureEndpoints = { ...@@ -18,7 +18,7 @@ const fixtureEndpoints = {
}; };
export const endpoints = { export const endpoints = {
groupLabels: /groups\/[A-Z|a-z|\d|\-|_]+\/labels/, groupLabels: /groups\/[A-Z|a-z|\d|\-|_]+\/-\/labels.json/,
summaryData: /analytics\/value_stream_analytics\/summary/, summaryData: /analytics\/value_stream_analytics\/summary/,
durationData: /analytics\/value_stream_analytics\/stages\/\d+\/duration_chart/, durationData: /analytics\/value_stream_analytics\/stages\/\d+\/duration_chart/,
stageData: /analytics\/value_stream_analytics\/stages\/\d+\/records/, stageData: /analytics\/value_stream_analytics\/stages\/\d+\/records/,
......
...@@ -185,69 +185,6 @@ describe('Cycle analytics actions', () => { ...@@ -185,69 +185,6 @@ describe('Cycle analytics actions', () => {
}); });
}); });
describe('fetchGroupLabels', () => {
describe('succeeds', () => {
beforeEach(() => {
gon.api_version = 'v4';
state = { selectedGroup };
mock.onGet(endpoints.groupLabels).replyOnce(200, groupLabels);
});
it('dispatches receiveGroupLabels if the request succeeds', () => {
return testAction(
actions.fetchGroupLabels,
null,
state,
[],
[
{ type: 'requestGroupLabels' },
{
type: 'receiveGroupLabelsSuccess',
payload: groupLabels,
},
],
);
});
});
describe('with an error', () => {
beforeEach(() => {
state = { selectedGroup };
mock.onGet(endpoints.groupLabels).replyOnce(404);
});
it('dispatches receiveGroupLabelsError if the request fails', () => {
return testAction(
actions.fetchGroupLabels,
null,
state,
[],
[
{ type: 'requestGroupLabels' },
{
type: 'receiveGroupLabelsError',
payload: error,
},
],
);
});
});
describe('receiveGroupLabelsError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('flashes an error message if the request fails', () => {
actions.receiveGroupLabelsError({
commit: () => {},
});
shouldFlashAMessage('There was an error fetching label data for the selected group');
});
});
});
describe('fetchTopRankedGroupLabels', () => { describe('fetchTopRankedGroupLabels', () => {
beforeEach(() => { beforeEach(() => {
gon.api_version = 'v4'; gon.api_version = 'v4';
...@@ -334,7 +271,6 @@ describe('Cycle analytics actions', () => { ...@@ -334,7 +271,6 @@ describe('Cycle analytics actions', () => {
const mocks = { const mocks = {
requestCycleAnalyticsData: requestCycleAnalyticsData:
overrides.requestCycleAnalyticsData || jest.fn().mockResolvedValue(), overrides.requestCycleAnalyticsData || jest.fn().mockResolvedValue(),
fetchGroupLabels: overrides.fetchGroupLabels || jest.fn().mockResolvedValue(),
fetchStageMedianValues: overrides.fetchStageMedianValues || jest.fn().mockResolvedValue(), fetchStageMedianValues: overrides.fetchStageMedianValues || jest.fn().mockResolvedValue(),
fetchGroupStagesAndEvents: fetchGroupStagesAndEvents:
overrides.fetchGroupStagesAndEvents || jest.fn().mockResolvedValue(), overrides.fetchGroupStagesAndEvents || jest.fn().mockResolvedValue(),
...@@ -347,7 +283,6 @@ describe('Cycle analytics actions', () => { ...@@ -347,7 +283,6 @@ describe('Cycle analytics actions', () => {
mockDispatchContext: jest mockDispatchContext: jest
.fn() .fn()
.mockImplementationOnce(mocks.requestCycleAnalyticsData) .mockImplementationOnce(mocks.requestCycleAnalyticsData)
.mockImplementationOnce(mocks.fetchGroupLabels)
.mockImplementationOnce(mocks.fetchGroupStagesAndEvents) .mockImplementationOnce(mocks.fetchGroupStagesAndEvents)
.mockImplementationOnce(mocks.fetchStageMedianValues) .mockImplementationOnce(mocks.fetchStageMedianValues)
.mockImplementationOnce(mocks.fetchSummaryData) .mockImplementationOnce(mocks.fetchSummaryData)
...@@ -369,7 +304,6 @@ describe('Cycle analytics actions', () => { ...@@ -369,7 +304,6 @@ describe('Cycle analytics actions', () => {
[], [],
[ [
{ type: 'requestCycleAnalyticsData' }, { type: 'requestCycleAnalyticsData' },
{ type: 'fetchGroupLabels' },
{ type: 'fetchGroupStagesAndEvents' }, { type: 'fetchGroupStagesAndEvents' },
{ type: 'fetchStageMedianValues' }, { type: 'fetchStageMedianValues' },
{ type: 'fetchSummaryData' }, { type: 'fetchSummaryData' },
...@@ -379,34 +313,6 @@ describe('Cycle analytics actions', () => { ...@@ -379,34 +313,6 @@ describe('Cycle analytics actions', () => {
); );
}); });
// TOOD: parameterize?
it(`displays an error if fetchGroupLabels fails`, done => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchGroupLabels: actions.fetchGroupLabels({
dispatch: jest
.fn()
.mockResolvedValueOnce()
.mockImplementation(actions.receiveGroupLabelsError({ commit: () => {} })),
commit: () => {},
state: { ...state },
getters,
}),
});
actions
.fetchCycleAnalyticsData({
dispatch: mockDispatchContext,
state: {},
commit: () => {},
})
.then(() => {
shouldFlashAMessage('There was an error fetching label data for the selected group');
done();
})
.catch(done.fail);
});
it(`displays an error if fetchStageMedianValues fails`, done => { it(`displays an error if fetchStageMedianValues fails`, done => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({ const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchStageMedianValues: actions.fetchStageMedianValues({ fetchStageMedianValues: actions.fetchStageMedianValues({
......
...@@ -13,7 +13,6 @@ import { ...@@ -13,7 +13,6 @@ import {
stagingStage, stagingStage,
reviewStage, reviewStage,
totalStage, totalStage,
groupLabels,
startDate, startDate,
endDate, endDate,
customizableStagesAndEvents, customizableStagesAndEvents,
...@@ -51,8 +50,6 @@ describe('Cycle analytics mutations', () => { ...@@ -51,8 +50,6 @@ describe('Cycle analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true} ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.REQUEST_GROUP_LABELS} | ${'labels'} | ${[]}
${types.RECEIVE_GROUP_LABELS_ERROR} | ${'labels'} | ${[]}
${types.REQUEST_TOP_RANKED_GROUP_LABELS} | ${'topRankedLabels'} | ${[]} ${types.REQUEST_TOP_RANKED_GROUP_LABELS} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR} | ${'topRankedLabels'} | ${[]} ${types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]} ${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]}
...@@ -148,22 +145,6 @@ describe('Cycle analytics mutations', () => { ...@@ -148,22 +145,6 @@ describe('Cycle analytics mutations', () => {
}); });
}); });
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);
expect(state.labels).toEqual(groupLabels.map(convertObjectPropsToCamelCase));
});
});
describe(`${types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS}`, () => {
it('will set the labels state item with the camelCased group labels', () => {
mutations[types.RECEIVE_GROUP_LABELS_SUCCESS](state, groupLabels);
expect(state.labels).toEqual(groupLabels.map(convertObjectPropsToCamelCase));
});
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => { describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => {
it('will set isLoading=false and errorCode=null', () => { it('will set isLoading=false and errorCode=null', () => {
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, { mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
......
...@@ -537,7 +537,8 @@ describe('Api', () => { ...@@ -537,7 +537,8 @@ describe('Api', () => {
describe('cycleAnalyticsGroupLabels', () => { describe('cycleAnalyticsGroupLabels', () => {
it('fetches group level labels', done => { it('fetches group level labels', done => {
const response = []; const response = [];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/labels`; const expectedUrl = `${dummyUrlRoot}/groups/${groupId}/-/labels.json`;
mock.onGet(expectedUrl).reply(httpStatus.OK, response); mock.onGet(expectedUrl).reply(httpStatus.OK, response);
Api.cycleAnalyticsGroupLabels(groupId) Api.cycleAnalyticsGroupLabels(groupId)
......
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