Commit 5f6492f7 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Kushal Pandya

Combine both label dropdowns

Rewrites the label_selector component to include
slots for the extra content needed for the
tasks_by_type labels dropdown
parent 295a69bf
......@@ -65,7 +65,6 @@ export default {
'selectedStage',
'stages',
'summary',
'labels',
'topRankedLabels',
'currentStageEvents',
'customStageFormEvents',
......@@ -302,7 +301,6 @@ export default {
:current-stage-events="currentStageEvents"
:custom-stage-form-events="customStageFormEvents"
:custom-stage-form-errors="customStageFormErrors"
:labels="labels"
:no-data-svg-path="noDataSvgPath"
:no-access-svg-path="noAccessSvgPath"
:can-edit-stages="hasCustomizableCycleAnalytics"
......@@ -350,7 +348,6 @@ export default {
<tasks-by-type-chart
:chart-data="tasksByTypeChartData"
:filters="selectedTasksByTypeFilters"
:labels="labels"
@updateFilter="setTasksByTypeFilters"
/>
</div>
......
......@@ -72,10 +72,6 @@ export default {
type: Array,
required: true,
},
labels: {
type: Array,
required: true,
},
initialFields: {
type: Object,
required: false,
......@@ -326,8 +322,7 @@ export default {
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:labels="labels"
:selected-label-id="fields.startEventLabelId"
:selected-label-id="[fields.startEventLabelId]"
name="custom-stage-start-event-label"
@selectLabel="handleSelectLabel('startEventLabelId', $event)"
@clearLabel="handleClearLabel('startEventLabelId')"
......@@ -363,8 +358,7 @@ export default {
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:labels="labels"
:selected-label-id="fields.endEventLabelId"
:selected-label-id="[fields.endEventLabelId]"
name="custom-stage-stop-event-label"
@selectLabel="handleSelectLabel('endEventLabelId', $event)"
@clearLabel="handleClearLabel('endEventLabelId')"
......
<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 {
name: 'LabelsSelector',
components: {
GlDropdown,
GlDropdownItem,
GlIcon,
GlLoadingIcon,
GlSearchBoxByType,
},
props: {
labels: {
defaultSelectedLabelIds: {
type: Array,
required: true,
required: false,
default: () => [],
},
selectedLabelId: {
maxLabels: {
type: Number,
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: {
selectedLabel() {
const { selectedLabelId, labels } = this;
if (!selectedLabelId || !labels.length) return null;
return labels.find(({ id }) => id === selectedLabelId);
const { selectedLabelId, labels = [] } = this;
if (!selectedLabelId.length || !labels.length) return null;
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: {
...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) {
// there are 2 possible endpoints for group labels
// one returns label.name the other label.title
return label?.name || label.title;
},
isSelectedLabel(id) {
return this.selectedLabelId && id === this.selectedLabelId;
return Boolean(this.selectedLabelId?.includes(id));
},
isDisabledLabel(id) {
return Boolean(this.maxLabelsSelected && !this.isSelectedLabel(id));
},
},
};
</script>
<template>
<gl-dropdown class="w-100" toggle-class="overflow-hidden">
<template slot="button-content">
<span v-if="selectedLabel">
<span
:style="{ backgroundColor: selectedLabel.color }"
class="d-inline-block dropdown-label-box"
>
<gl-dropdown class="w-100" toggle-class="overflow-hidden" :right="right">
<template #button-content>
<slot name="label-dropdown-button">
<span v-if="selectedLabel">
<span
:style="{ backgroundColor: selectedLabel.color }"
class="d-inline-block dropdown-label-box"
>
</span>
{{ labelTitle(selectedLabel) }}
</span>
{{ labelTitle(selectedLabel) }}
</span>
<span v-else>{{ __('Select a label') }}</span>
<span v-else>{{ __('Select a label') }}</span>
</slot>
</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>
<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>
</template>
......@@ -72,10 +72,6 @@ export default {
required: false,
default: () => {},
},
labels: {
type: Array,
required: true,
},
noDataSvgPath: {
type: String,
required: true,
......@@ -230,7 +226,6 @@ export default {
<custom-stage-form
v-else-if="isCreatingCustomStage || isEditingCustomStage"
:events="customStageFormEvents"
:labels="labels"
:is-saving-custom-stage="isSavingCustomStage"
:initial-fields="customStageFormInitialData"
:is-editing-custom-stage="isEditingCustomStage"
......
......@@ -20,10 +20,6 @@ export default {
type: Object,
required: true,
},
labels: {
type: Array,
required: true,
},
},
computed: {
hasData() {
......@@ -69,7 +65,6 @@ export default {
<div v-if="hasData">
<p>{{ summaryDescription }}</p>
<tasks-by-type-filters
:labels="labels"
:selected-label-ids="filters.selectedLabelIds"
:subject-filter="selectedSubjectFilter"
@updateFilter="$emit('updateFilter', $event)"
......
<script>
import {
GlDropdownDivider,
GlSegmentedControl,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
import { GlDropdownDivider, GlSegmentedControl, GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { removeFlash } from '../utils';
......@@ -16,6 +9,7 @@ import {
TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS,
TASKS_BY_TYPE_MAX_LABELS,
} from '../constants';
import LabelsSelector from './labels_selector.vue';
export default {
name: 'TasksByTypeFilters',
......@@ -23,37 +17,23 @@ export default {
GlSegmentedControl,
GlDropdownDivider,
GlIcon,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
LabelsSelector,
},
props: {
maxLabels: {
type: Number,
required: false,
default: TASKS_BY_TYPE_MAX_LABELS,
},
labels: {
selectedLabelIds: {
type: Array,
required: true,
},
selectedLabelIds: {
type: Array,
maxLabels: {
type: Number,
required: false,
default: () => [],
default: TASKS_BY_TYPE_MAX_LABELS,
},
subjectFilter: {
type: String,
required: true,
},
},
data() {
const { subjectFilter: selectedSubjectFilter } = this;
return {
selectedSubjectFilter,
labelsSearchTerm: '',
};
},
computed: {
subjectFilterOptions() {
return Object.entries(TASKS_BY_TYPE_SUBJECT_FILTER_OPTIONS).map(([value, text]) => ({
......@@ -63,10 +43,9 @@ export default {
},
selectedFiltersText() {
const { subjectFilter, selectedLabelIds } = this;
const subjectFilterText =
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];
const subjectFilterText = 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];
return sprintf(
s__('CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCount} labels'),
{
......@@ -75,11 +54,6 @@ export default {
},
);
},
availableLabels() {
return this.labels.filter(({ name }) =>
name.toLowerCase().includes(this.labelsSearchTerm.toLowerCase()),
);
},
selectedLabelLimitText() {
const { selectedLabelIds, maxLabels } = this;
return sprintf(s__('CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)'), {
......@@ -90,21 +64,12 @@ export default {
maxLabelsSelected() {
return this.selectedLabelIds.length >= this.maxLabels;
},
hasMatchingLabels() {
return this.availableLabels.length;
},
},
methods: {
canUpdateLabelFilters(value) {
// we can always remove a filter
return this.selectedLabelIds.includes(value) || !this.maxLabelsSelected;
},
isLabelSelected(id) {
return this.selectedLabelIds.includes(id);
},
isLabelDisabled(id) {
return this.maxLabelsSelected && !this.isLabelSelected(id);
},
handleLabelSelected(value) {
removeFlash('notice');
if (this.canUpdateLabelFilters(value)) {
......@@ -132,59 +97,41 @@ export default {
<p>{{ selectedFiltersText }}</p>
</div>
<div class="flex-column">
<gl-dropdown
aria-expanded="false"
<labels-selector
:default-selected-labels-ids="selectedLabelIds"
:max-labels="maxLabels"
:aria-label="__('CycleAnalytics|Display chart filters')"
:selected-label-id="selectedLabelIds"
aria-expanded="false"
multiselect
right
@selectLabel="handleLabelSelected"
>
<template #button-content>
<template #label-dropdown-button>
<gl-icon class="vertical-align-top" name="settings" />
<gl-icon name="chevron-down" />
</template>
<div class="mb-3 px-3">
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
<gl-segmented-control
v-model="selectedSubjectFilter"
:options="subjectFilterOptions"
@input="
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"
<template #label-dropdown-list-header>
<div class="mb-3 px-3">
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
<gl-segmented-control
:checked="subjectFilter"
:options="subjectFilterOptions"
@input="
value =>
$emit('updateFilter', { filter: $options.TASKS_BY_TYPE_FILTERS.SUBJECT, value })
"
/>
<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>
</gl-dropdown>
<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>
</div>
</template>
</labels-selector>
</div>
</div>
</template>
......@@ -132,7 +132,6 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
dispatch('requestCycleAnalyticsData');
return Promise.resolve()
.then(() => dispatch('fetchGroupLabels'))
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('fetchSummaryData'))
......@@ -207,29 +206,6 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => {
export const requestGroupStagesAndEvents = ({ commit }) =>
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) => {
commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS, data);
dispatch('fetchTasksByTypeData');
......
......@@ -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 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 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';
......
......@@ -56,15 +56,6 @@ export default {
state.isEmptyStage = true;
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) {
state.topRankedLabels = [];
state.tasksByType = {
......
......@@ -29,7 +29,6 @@ export default () => ({
stages: [],
summary: [],
labels: [],
topRankedLabels: [],
medians: {},
......
......@@ -22,7 +22,7 @@ export default {
cycleAnalyticsStagePath: '/-/analytics/value_stream_analytics/stages/:stage_id',
cycleAnalyticsDurationChartPath:
'/-/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',
groupActivityIssuesPath: '/api/:version/analytics/group_activity/issues_count',
groupActivityMergeRequestsPath: '/api/:version/analytics/group_activity/merge_requests_count',
......@@ -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
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25746
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'
describe 'Group Value Stream Analytics', :js do
include DragTo
let!(:user) { create(:user) }
let!(:group) { create(:group, name: "CA-test-group") }
let!(:sub_group) { create(:group, name: "CA-sub-group", parent: group) }
let!(:group2) { create(:group, name: "CA-bad-test-group") }
let!(:project) { create(:project, :repository, namespace: group, group: group, name: "Cool fun project") }
let!(:label) { create(:group_label, group: group) }
let!(:label2) { create(:group_label, group: group) }
let!(:label3) { create(:group_label, group: group2) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, name: "CA-test-group") }
let_it_be(:sub_group) { create(:group, name: "CA-sub-group", parent: group) }
let_it_be(:group2) { create(:group, name: "CA-bad-test-group") }
let_it_be(:project) { create(:project, :repository, namespace: group, group: group, name: "Cool fun project") }
let_it_be(:group_label1) { create(:group_label, group: group) }
let_it_be(:group_label2) { create(:group_label, group: group) }
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(: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
stage_nav_selector = '.stage-nav'
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
shared_examples 'empty state' do
......@@ -268,7 +270,7 @@ describe 'Group Value Stream Analytics', :js do
end
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|
Timecop.freeze { example.run }
......@@ -350,8 +352,8 @@ describe 'Group Value Stream Analytics', :js do
context 'with data available' do
before do
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: [label2])
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: [group_label2])
end
visit analytics_cycle_analytics_path
......@@ -436,8 +438,17 @@ describe 'Group Value Stream Analytics', :js do
page.find("select[name='#{name}']").find("#{elem}[value=#{value}]").select_option
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)
page.find("[name=#{field}] .dropdown-toggle").click
page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[index].click
end
......@@ -626,24 +637,30 @@ describe 'Group Value Stream Analytics', :js do
expect(page).to have_button('Add stage', disabled: true)
end
it 'does not contain labels from outside the group' do
field = 'custom-stage-start-event-label'
page.find("[name=#{field}] .dropdown-toggle").click
menu = page.find("[name=#{field}] .dropdown-menu")
context 'with labels available' do
start_field = "custom-stage-start-event-label"
end_field = "custom-stage-stop-event-label"
expect(menu).not_to have_content(label3.name)
expect(menu).to have_content(label.name)
expect(menu).to have_content(label2.name)
end
it 'does not contain labels from outside the group' do
wait_for_labels(start_field)
menu = page.find("[name=#{start_field}] .dropdown-menu")
context 'with all required fields set' do
before do
select_dropdown_label 'custom-stage-start-event-label', 1
select_dropdown_label 'custom-stage-stop-event-label', 2
expect(menu).not_to have_content(other_label.name)
expect(menu).to have_content(first_label.name)
expect(menu).to have_content(second_label.name)
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
......@@ -725,7 +742,11 @@ describe 'Group Value Stream Analytics', :js do
select_group
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
context 'with a custom stage created' do
......@@ -746,7 +767,11 @@ describe 'Group Value Stream Analytics', :js do
select_group(sub_group.full_name)
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
context 'with a custom stage created' do
......
......@@ -6,52 +6,6 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
</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`] = `
"<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
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
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>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</gl-dropdown-item-stub>
<gl-dropdown-item-stub><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><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</gl-dropdown-item-stub>
<div class=\\"mb-3 px-3\\">
<gl-search-box-by-type-stub value=\\"\\" clearbuttontitle=\\"Clear\\" class=\\"mb-2\\"></gl-search-box-by-type-stub>
</div>
<div class=\\"mb-3 px-3\\">
<gl-dropdown-item-stub class=\\"\\">
<!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</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>"
`;
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>
<gl-dropdown-item-stub><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</gl-dropdown-item-stub>
<gl-dropdown-item-stub><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\\"><span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(0, 0, 255);\\"></span>
violets
</gl-dropdown-item-stub>
<div class=\\"mb-3 px-3\\">
<gl-search-box-by-type-stub value=\\"\\" clearbuttontitle=\\"Clear\\" class=\\"mb-2\\"></gl-search-box-by-type-stub>
</div>
<div class=\\"mb-3 px-3\\">
<gl-dropdown-item-stub class=\\"\\">
<!----> <span class=\\"d-inline-block dropdown-label-box\\" style=\\"background-color: rgb(255, 0, 0);\\"></span>
roses
</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>"
`;
......@@ -17,7 +17,7 @@ exports[`TasksByTypeChart with data available should render the loading chart 1`
<h3>Type of work</h3>
<div>
<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>
</div>
</div>
......
......@@ -38,6 +38,7 @@ const defaultStubs = {
'stage-event-list': true,
'stage-nav-item': true,
'tasks-by-type-chart': true,
'labels-selector': true,
};
function createComponent({
......@@ -383,7 +384,6 @@ describe('Cycle Analytics component', () => {
describe('with tasksByTypeChart=true', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({
shallow: false,
withStageSelected: true,
......@@ -394,7 +394,6 @@ describe('Cycle Analytics component', () => {
afterEach(() => {
wrapper.destroy();
mock.restore();
});
it('displays the tasks by type chart', () => {
......@@ -446,11 +445,6 @@ describe('Cycle Analytics component', () => {
endpoint: mockData.endpoints.baseStagesEndpoint,
response: { ...mockData.customizableStagesAndEvents },
},
fetchGroupLabels: {
status: defaultStatus,
endpoint: mockData.endpoints.groupLabels,
response: [...mockData.groupLabels],
},
...overrides,
};
......@@ -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', () => {
expect(findFlashError()).toBeNull();
......
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import createStore from 'ee/analytics/cycle_analytics/store';
import { createLocalVue, mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import CustomStageForm, {
initializeFormData,
} from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants';
import {
endpoints,
groupLabels,
customStageEvents as events,
labelStartEvent,
......@@ -31,6 +35,7 @@ const MERGE_REQUEST_CLOSED = 'merge_request_closed';
let store = null;
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('lodash/debounce', () => jest.fn);
describe('CustomStageForm', () => {
function createComponent(props = {}, stubs = {}) {
......@@ -40,14 +45,18 @@ describe('CustomStageForm', () => {
store,
propsData: {
events,
labels: groupLabels,
...props,
},
stubs,
stubs: {
'labels-selector': false,
...stubs,
},
});
}
let wrapper = null;
let mock;
const findEvent = ev => wrapper.emitted()[ev];
const sel = {
......@@ -104,12 +113,17 @@ describe('CustomStageForm', () => {
return _wrapper.vm.$nextTick();
}
const mockGroupLabelsRequest = () =>
new MockAdapter(axios).onGet(endpoints.groupLabels).reply(200, groupLabels);
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe.each([
......@@ -170,14 +184,20 @@ describe('CustomStageForm', () => {
it('selects events with canBeStartEvent=true for the start events dropdown', () => {
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', () => {
const select = wrapper.find(sel.startEvent);
expect(select.html()).toMatchSnapshot();
stopEvents
events
.filter(ev => !ev.canBeStartEvent)
.forEach(ev => {
expect(select.html()).not.toHaveHtml(
......@@ -189,7 +209,10 @@ describe('CustomStageForm', () => {
describe('start event label', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent();
return wrapper.vm.$nextTick();
});
afterEach(() => {
......@@ -217,14 +240,13 @@ describe('CustomStageForm', () => {
expect(wrapper.vm.fields.startEventLabelId).toEqual(null);
wrapper.find(sel.startEvent).setValue(labelStartEvent.identifier);
return Vue.nextTick()
return waitForPromises()
.then(() => {
wrapper
.find(sel.startEventLabel)
.findAll('.dropdown-item')
.at(1) // item at index 0 is 'select a label'
.trigger('click');
return Vue.nextTick();
})
.then(() => {
......@@ -400,7 +422,7 @@ describe('CustomStageForm', () => {
},
});
return Vue.nextTick()
return waitForPromises()
.then(() => {
wrapper
.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 { groupLabels } from '../mock_data';
const selectedLabel = groupLabels[groupLabels.length - 1];
const findActiveItem = wrapper =>
wrapper
.findAll('gl-dropdown-item-stub')
.filter(d => d.attributes('active'))
.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', () => {
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;
return func(LabelsSelector, {
localVue,
store: {
...store,
getters: {
...getters,
currentGroupPath: 'fake',
},
},
propsData: {
labels: groupLabels,
selectedLabelId: props.selectedLabelId || null,
...props,
},
});
}
let wrapper = null;
let mock = null;
const labelNames = groupLabels.map(({ name }) => name);
describe('with no item selected', () => {
beforeEach(() => {
wrapper = createComponent();
mock = mockGroupLabelsRequest();
wrapper = createComponent({});
return waitForPromises();
});
afterEach(() => {
mock.restore();
wrapper.destroy();
wrapper = null;
});
......@@ -49,9 +78,27 @@ describe('Value Stream Analytics LabelsSelector', () => {
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', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({ shallow: false });
return waitForPromises();
});
it('will emit the "selectLabel" event', () => {
......@@ -81,7 +128,9 @@ describe('Value Stream Analytics LabelsSelector', () => {
describe('with selectedLabelId set', () => {
beforeEach(() => {
wrapper = createComponent({ props: { selectedLabelId: selectedLabel.id } });
mock = mockGroupLabelsRequest();
wrapper = createComponent({ props: { selectedLabelId: [selectedLabel.id] } });
return waitForPromises();
});
afterEach(() => {
......
import { mount, shallowMount } from '@vue/test-utils';
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 { groupLabels } from '../mock_data';
const seriesNames = ['Cool label', 'Normal label'];
const data = [[0, 1, 2], [5, 2, 3], [2, 4, 1]];
......@@ -30,7 +29,6 @@ function createComponent({ props = {}, shallow = true, stubs = {} }) {
data,
seriesNames,
},
labels: groupLabels,
...props,
},
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 TasksByTypeFilters from 'ee/analytics/cycle_analytics/components/tasks_by_type_filters.vue';
import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue';
import {
TASKS_BY_TYPE_SUBJECT_ISSUE,
TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST,
TASKS_BY_TYPE_FILTERS,
} from 'ee/analytics/cycle_analytics/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { shouldFlashAMessage } from '../helpers';
import { groupLabels } from '../mock_data';
import createStore from 'ee/analytics/cycle_analytics/store';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
const selectedLabelIds = [groupLabels[0].id];
const findSubjectFilters = ctx => ctx.find(GlSegmentedControl);
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) => {
findDropdownLabels(ctx)
.at(index)
.vm.$emit('click');
return ctx.vm.$nextTick();
.trigger('click');
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 }) {
store = createStore();
return mountFn(TasksByTypeFilters, {
localVue,
store: {
...store,
getters: {
...getters,
currentGroupPath: 'fake',
},
},
propsData: {
selectedLabelIds,
labels: groupLabels,
......@@ -31,42 +55,50 @@ function createComponent({ props = {}, mountFn = shallowMount }) {
...props,
},
stubs: {
GlNewDropdown: true,
GlDropdownItem: true,
LabelsSelector,
},
});
}
describe('TasksByTypeFilters', () => {
let wrapper = null;
let mock = null;
beforeEach(() => {
mock = mockGroupLabelsRequest();
wrapper = createComponent({});
return waitForPromises();
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
describe('labels', () => {
beforeEach(() => {
mock = mockGroupLabelsRequest();
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();
return selectLabelAtIndex(wrapper, 0).then(() => {
expect(wrapper.emitted('updateFilter')).toBeDefined();
expect(wrapper.emitted('updateFilter')[0]).toEqual([
{ filter: TASKS_BY_TYPE_FILTERS.LABEL, value: groupLabels[0].id },
]);
});
wrapper.find(LabelsSelector).vm.$emit('selectLabel', 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', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest();
wrapper = createComponent({
props: {
maxLabels: 5,
......@@ -75,7 +107,7 @@ describe('TasksByTypeFilters', () => {
},
});
return selectLabelAtIndex(wrapper, 2);
return waitForPromises().then(() => selectLabelAtIndex(wrapper, 2));
});
it('should indicate how many labels are selected', () => {
......@@ -86,6 +118,8 @@ describe('TasksByTypeFilters', () => {
describe('with maximum labels selected', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock = mockGroupLabelsRequest();
wrapper = createComponent({
props: {
maxLabels: 2,
......@@ -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', () => {
......
......@@ -18,7 +18,7 @@ const fixtureEndpoints = {
};
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/,
durationData: /analytics\/value_stream_analytics\/stages\/\d+\/duration_chart/,
stageData: /analytics\/value_stream_analytics\/stages\/\d+\/records/,
......
......@@ -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', () => {
beforeEach(() => {
gon.api_version = 'v4';
......@@ -334,7 +271,6 @@ describe('Cycle analytics actions', () => {
const mocks = {
requestCycleAnalyticsData:
overrides.requestCycleAnalyticsData || jest.fn().mockResolvedValue(),
fetchGroupLabels: overrides.fetchGroupLabels || jest.fn().mockResolvedValue(),
fetchStageMedianValues: overrides.fetchStageMedianValues || jest.fn().mockResolvedValue(),
fetchGroupStagesAndEvents:
overrides.fetchGroupStagesAndEvents || jest.fn().mockResolvedValue(),
......@@ -347,7 +283,6 @@ describe('Cycle analytics actions', () => {
mockDispatchContext: jest
.fn()
.mockImplementationOnce(mocks.requestCycleAnalyticsData)
.mockImplementationOnce(mocks.fetchGroupLabels)
.mockImplementationOnce(mocks.fetchGroupStagesAndEvents)
.mockImplementationOnce(mocks.fetchStageMedianValues)
.mockImplementationOnce(mocks.fetchSummaryData)
......@@ -369,7 +304,6 @@ describe('Cycle analytics actions', () => {
[],
[
{ type: 'requestCycleAnalyticsData' },
{ type: 'fetchGroupLabels' },
{ type: 'fetchGroupStagesAndEvents' },
{ type: 'fetchStageMedianValues' },
{ type: 'fetchSummaryData' },
......@@ -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 => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchStageMedianValues: actions.fetchStageMedianValues({
......
......@@ -13,7 +13,6 @@ import {
stagingStage,
reviewStage,
totalStage,
groupLabels,
startDate,
endDate,
customizableStagesAndEvents,
......@@ -51,8 +50,6 @@ describe('Cycle analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${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.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]}
......@@ -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}`, () => {
it('will set isLoading=false and errorCode=null', () => {
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
......
......@@ -537,7 +537,8 @@ describe('Api', () => {
describe('cycleAnalyticsGroupLabels', () => {
it('fetches group level labels', done => {
const response = [];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/labels`;
const expectedUrl = `${dummyUrlRoot}/groups/${groupId}/-/labels.json`;
mock.onGet(expectedUrl).reply(httpStatus.OK, response);
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