Commit ff8bc953 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added label selector

Bootstrap group_labels for the project

Moved to new analytics ee dir

Added container to provide label data

Update the selected label

Render custom button text when label is selected

Ensure label width stays within its container

Added tests for the labels_selector component

Replace GlLabel with a span tag

Fix css classes, fix event handlers

Finished specs for start/stop label events
parent a6e0d598
<script>
import { s__ } from '~/locale';
import { GlButton, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
const isStartEvent = ev => ev && ev.can_be_start_event;
const eventToOption = ({ name: text = '', identifier: value = null }) => ({
text,
value,
});
const getAllowedStopEvents = (events = [], targetIdentifier = null) => {
if (!targetIdentifier || !events.length) return [];
const st = events.find(({ identifier }) => identifier === targetIdentifier);
return st.allowed_end_events;
};
const eventsByIdentifier = (events = [], targetIdentifier = []) => {
if (!targetIdentifier.length || !events.length) return [];
return events.filter(({ identifier }) => targetIdentifier.indexOf(identifier) > -1);
};
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlFormSelect,
},
props: {
events: {
type: Array,
required: true,
},
// name: {
// type: String,
// default: null,
// },
// objectType: {
// type: String,
// default: null,
// },
// startEvent: {
// type: String,
// default: null,
// },
// stopEvent: {
// type: String,
// default: null,
// },
},
data() {
return {
fields: {
// objectType: null,
name: '',
startEvent: '',
startEventLabel: '',
stopEvent: '',
stopEventLabel: '',
},
};
},
computed: {
stopEventOptions() {
const stopEvents = getAllowedStopEvents(this.events, this.fields.startEvent);
return [
{ value: null, text: s__('CustomCycleAnalytics|Select stop event') },
...eventsByIdentifier(this.events, stopEvents).map(eventToOption),
];
},
startEventOptions() {
return [
{ value: null, text: s__('CustomCycleAnalytics|Select start event') },
...this.events.filter(isStartEvent).map(eventToOption),
];
},
hasStartEvent() {
return this.fields.startEvent;
},
// startEventRequiresLabel() {},
// stopEventRequiresLabel() {},
isComplete() {
// TODO: need to factor in label field
const requiredFields = [this.fields.startEvent, this.fields.stopEvent, this.fields.name];
return requiredFields.every(fieldValue => fieldValue && fieldValue.length > 0);
},
},
methods: {
handleSave() {
this.$emit('submit', this.fields);
},
},
};
</script>
<template>
<form class="add-stage-form m-4">
<div class="mb-1">
<h4>{{ s__('CustomCycleAnalytics|New stage') }}</h4>
</div>
<gl-form-group :label="s__('CustomCycleAnalytics|Name')">
<gl-form-input
v-model="fields.name"
class="form-control"
type="text"
value=""
name="add-stage-name"
:placeholder="s__('CustomCycleAnalytics|Enter a name for the stage')"
required
/>
</gl-form-group>
<gl-form-group :label="s__('CustomCycleAnalytics|Start event')">
<gl-form-select
v-model="fields.startEvent"
name="add-stage-start-event"
:required="true"
:options="startEventOptions"
/>
</gl-form-group>
<gl-form-group
:label="s__('CustomCycleAnalytics|Stop event')"
:description="s__('CustomCycleAnalytics|Please select a start event first')"
>
<gl-form-select
v-model="fields.stopEvent"
name="add-stage-stop-event"
:options="stopEventOptions"
:required="true"
:disabled="!hasStartEvent"
/>
</gl-form-group>
<div class="add-stage-form-actions">
<!--
TODO: what does the cancel button do?
- Just hide the form?
- clear entered data?
-->
<button class="btn btn-cancel add-stage-cancel" type="button" @click="cancelHandler()">
{{ __('Cancel') }}
</button>
<button
:disabled="!isComplete"
type="button"
class="js-add-stage btn btn-success"
@click="handleSave"
>
{{ s__('CustomCycleAnalytics|Add stage') }}
</button>
</div>
</form>
</template>
......@@ -49,6 +49,8 @@ export default () => {
import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'),
AddStageButton: () =>
import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'),
CustomStageFormContainer: () =>
import('ee_component/analytics/cycle_analytics/components/custom_stage_form_container.vue'),
},
mixins: [filterMixins, addStageMixin],
data() {
......@@ -77,6 +79,14 @@ export default () => {
// variable itself can be completely removed.
// Follow up issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/64490
if (cycleAnalyticsEl.dataset.requestPath) this.fetchCycleAnalyticsData();
console.log('LOADING::data', cycleAnalyticsEl.dataset);
// groupLabels needed for label dropdown in the custom stages form
const groupLabels = cycleAnalyticsEl.dataset.groupLabels
? JSON.parse(cycleAnalyticsEl.dataset.groupLabels)
: [];
this.store.setGroupLabels(groupLabels);
},
methods: {
handleError() {
......
......@@ -35,6 +35,7 @@ export default {
analytics: '',
events: [],
stages: [],
groupLabels: [],
},
setCycleAnalyticsData(data) {
this.state = Object.assign(this.state, this.decorateData(data));
......@@ -112,4 +113,13 @@ export default {
currentActiveStage() {
return this.state.stages.find(stage => stage.active);
},
setGroupLabels(labels) {
this.state.groupLabels = labels.map(({ id, title, color, textColor, description }) => ({
id,
title,
color,
textColor,
description,
}));
},
};
......@@ -68,7 +68,9 @@ export default {
:key="`${project.organizationSlug}.${project.slug}`"
class="w-100"
@click="$emit('select-project', project)"
>{{ getDisplayName(project) }}</gl-dropdown-item
>
<div></div>{{ getDisplayName(project) }}
</gl-dropdown-item
>
</gl-dropdown>
</div>
......
......@@ -127,7 +127,7 @@ export default {
<template>
<div class="block labels js-labels-block">
<dropdown-value-collapsed
<!-- <dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
......@@ -140,8 +140,8 @@ export default {
:enable-scoped-labels="enableScopedLabels"
>
<slot></slot>
</dropdown-value>
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none;">
</dropdown-value> -->
<div class="selectbox js-selectbox">
<dropdown-hidden-input
v-for="label in context.labels"
:key="label.id"
......
- page_title "Cycle Analytics"
#cycle-analytics{ "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
%banner{ "v-if" => "!isOverviewDialogDismissed",
"documentation-link": help_page_path('user/project/cycle_analytics'),
......
<script>
import { s__, __ } from '~/locale';
import { GlButton, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import LabelsSelector from './labels_selector.vue';
const EVENT_TYPE_LABEL = 'label';
const isStartEvent = ev => ev && ev.can_be_start_event;
const eventToOption = ({ name: text = '', identifier: value = null }) => ({
text,
value,
});
const getAllowedStopEvents = (events = [], targetIdentifier = null) => {
if (!targetIdentifier || !events.length) return [];
const st = events.find(({ identifier }) => identifier === targetIdentifier);
return st.allowed_end_events;
};
const eventsByIdentifier = (events = [], targetIdentifier = []) => {
if (!targetIdentifier.length || !events.length) return [];
return events.filter(({ identifier }) => targetIdentifier.indexOf(identifier) > -1);
};
const isLabelEvent = (labelEvents = [], ev = null) =>
ev && labelEvents.length && labelEvents.includes(ev);
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlFormSelect,
LabelsSelector,
},
props: {
events: {
type: Array,
// required: true,
required: false,
default: () => [
{
name: __('Issue created'),
identifier: 'issue_created',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['issue_stage_end'],
},
{
name: __('Issue first mentioned in a commit'),
identifier: 'issue_first_mentioned_in_commit',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: __('Merge request created'),
identifier: 'merge_request_created',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_merged'],
},
{
name: __('Merge request first deployed to production'),
identifier: 'merge_request_first_deployed_to_production',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: __('Merge request last build finish time'),
identifier: 'merge_request_last_build_finished',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: __('Merge request last build start time'),
identifier: 'merge_request_last_build_started',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_last_build_finished'],
},
{
name: __('Merge request merged'),
identifier: 'merge_request_merged',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_first_deployed_to_production'],
},
{
name: __('Issue first mentioned in a commit'),
identifier: 'code_stage_start',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_created'],
},
{
name: __('Issue first associated with a milestone or issue first added to a board'),
identifier: 'issue_stage_end',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: __('Issue first associated with a milestone or issue first added to a board'),
identifier: 'plan_stage_start',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['issue_first_mentioned_in_commit'],
},
{
identifier: 'issue_label_added',
name: __('Issue Label Added'),
type: 'label',
can_be_start_event: true,
allowed_end_events: ['issue_closed', 'issue_label_removed'],
},
{
identifier: 'issue_label_removed',
name: __('Issue Label Removed'),
type: 'label',
can_be_start_event: false,
allowed_end_events: [],
},
],
},
labels: {
type: Array,
required: true,
},
},
data() {
return {
fields: {
name: '',
startEvent: '',
startEventLabel: null,
stopEvent: '',
stopEventLabel: null,
},
labelEvents: [],
};
},
computed: {
stopEventOptions() {
const stopEvents = getAllowedStopEvents(this.events, this.fields.startEvent);
return [
{ value: null, text: s__('CustomCycleAnalytics|Select stop event') },
...eventsByIdentifier(this.events, stopEvents).map(eventToOption),
];
},
startEventOptions() {
return [
{ value: null, text: s__('CustomCycleAnalytics|Select start event') },
...this.events.filter(isStartEvent).map(eventToOption),
];
},
hasStartEvent() {
return this.fields.startEvent;
},
startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEvent);
},
stopEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.stopEvent);
},
isComplete() {
const requiredFields = [this.fields.startEvent, this.fields.stopEvent, this.fields.name];
// TODO: need to factor in label field
return requiredFields.every(fieldValue => fieldValue && fieldValue.length > 0);
},
},
mounted() {
this.labelEvents = this.events
.filter(ev => ev.type === EVENT_TYPE_LABEL)
.map(i => i.identifier);
},
methods: {
handleSave() {
this.$emit('submit', this.fields);
},
handleSelectLabel(key, labelId = null) {
this.fields[key] = labelId;
},
handleClearLabel(key) {
this.fields[key] = null;
},
},
};
</script>
<template>
<div class="ml-3">
<h2>{{ s__('New stage') }}</h2>
</div>
<form class="add-stage-form m-4">
<div class="mb-1">
<h4>{{ s__('CustomCycleAnalytics|New stage') }}</h4>
</div>
<gl-form-group :label="s__('CustomCycleAnalytics|Name')">
<gl-form-input
v-model="fields.name"
class="form-control"
type="text"
value=""
name="add-stage-name"
:placeholder="s__('CustomCycleAnalytics|Enter a name for the stage')"
required
/>
</gl-form-group>
<div class="d-flex" :class="{ 'justify-content-between': startEventRequiresLabel }">
<div :class="[startEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group :label="s__('CustomCycleAnalytics|Start event')">
<gl-form-select
v-model="fields.startEvent"
name="add-stage-start-event"
:required="true"
:options="startEventOptions"
/>
</gl-form-group>
</div>
<div v-if="startEventRequiresLabel" class="w-50 ml-1">
<gl-form-group :label="s__('CustomCycleAnalytics|Start event label')">
<labels-selector
:labels="labels"
:selected-label-id="fields.startEventLabel"
name="add-stage-start-event-label"
@selectLabel="labelId => handleSelectLabel('startEventLabel', labelId)"
@clearLabel="handleClearLabel"
/>
</gl-form-group>
</div>
</div>
<div class="d-flex" :class="{ 'justify-content-between': stopEventRequiresLabel }">
<div :class="[stopEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group
:label="s__('CustomCycleAnalytics|Stop event')"
:description="s__('CustomCycleAnalytics|Please select a start event first')"
>
<gl-form-select
v-model="fields.stopEvent"
name="add-stage-stop-event"
:options="stopEventOptions"
:required="true"
:disabled="!hasStartEvent"
/>
</gl-form-group>
</div>
<div v-if="stopEventRequiresLabel" class="w-50 ml-1">
<gl-form-group :label="s__('CustomCycleAnalytics|Stop event label')">
<labels-selector
:labels="labels"
:selected-label-id="fields.stopEventLabel"
name="add-stage-stop-event-label"
@selectLabel="labelId => handleSelectLabel('stopEventLabel', labelId)"
@clearLabel="handleClearLabel"
/>
</gl-form-group>
</div>
</div>
<div class="add-stage-form-actions">
<!--
TODO: what does the cancel button do?
- Just hide the form?
- clear entered data?
-->
<button class="btn btn-cancel add-stage-cancel" type="button" @click="cancelHandler()">
{{ __('Cancel') }}
</button>
<button
:disabled="!isComplete"
type="button"
class="js-add-stage btn btn-success"
@click="handleSave"
>
{{ s__('CustomCycleAnalytics|Add stage') }}
</button>
</div>
</form>
</template>
<script>
// NOTE: this is a temporary component while cycle-analytics is being refactored
// post refactor we will have a vuex store and functionality to fetch data
// https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15039
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import CustomStageForm from './custom_stage_form.vue';
function fetchGroupLabels(groupId, query = {}) {
const { api_version: apiVersion } = window.gon;
const url = `/api/${apiVersion}/groups/${groupId}/labels`;
return axios.get(url, { ...query }).then(response => response.data);
}
export default {
name: 'CustomStageFormContainer',
components: {
CustomStageForm,
},
props: {
groupId: {
type: Number,
required: true,
},
},
data() {
return {
labels: [],
};
},
created() {
fetchGroupLabels(this.groupId)
.then(labels => {
this.labels = labels;
})
.catch(() => {
createFlash(__('There was an error fetching the form data'));
});
},
};
</script>
<template>
<custom-stage-form :labels="labels" />
</template>
......@@ -78,4 +78,5 @@
%template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage && !isCustomStageForm" }
%component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
- if customizable_cycle_analytics
%custom-stage-form{ "v-if" => "isCustomStageForm" }
%custom-stage-form-container{ "v-if" => "isCustomStageForm && selectedGroup && selectedGroup.id", ":group-id" => "selectedGroup.id" }
......@@ -84,4 +84,21 @@ describe('Cycle Analytics LabelsSelector', () => {
expect(activeItem.text()).toEqual(selectedLabel.name);
});
});
describe('with selectedLabelId set', () => {
beforeEach(() => {
wrapper = createComponent({
selectedLabelId: 55,
});
});
afterEach(() => {
wrapper.destroy();
});
it('will set the active class', () => {
const activeItem = wrapper.find('[active="true"]');
expect(activeItem.exists()).toBe(true);
expect(activeItem.text()).toEqual('workflow::this-is-a-label');
});
});
});
......@@ -51,3 +51,231 @@ export const codeEvents = stageFixtures.code;
export const testEvents = stageFixtures.test;
export const stagingEvents = stageFixtures.staging;
export const productionEvents = stageFixtures.production;
const apiResponse = {
events: [
{
name: 'Issue created',
identifier: 'issue_created',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['issue_stage_end'],
},
{
name: 'Issue first mentioned in a commit',
identifier: 'issue_first_mentioned_in_commit',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: 'Merge request created',
identifier: 'merge_request_created',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_merged'],
},
{
name: 'Merge request first deployed to production',
identifier: 'merge_request_first_deployed_to_production',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: 'Merge request last build finish time',
identifier: 'merge_request_last_build_finished',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: 'Merge request last build start time',
identifier: 'merge_request_last_build_started',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_last_build_finished'],
},
{
name: 'Merge request merged',
identifier: 'merge_request_merged',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_first_deployed_to_production'],
},
{
name: 'Issue first mentioned in a commit',
identifier: 'code_stage_start',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['merge_request_created'],
},
{
name: 'Issue first associated with a milestone or issue first added to a board',
identifier: 'issue_stage_end',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: 'Issue first associated with a milestone or issue first added to a board',
identifier: 'plan_stage_start',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['issue_first_mentioned_in_commit'],
},
{
identifier: 'issue_label_added',
name: 'Issue Label Added',
type: 'label',
can_be_start_event: true,
allowed_end_events: ['issue_closed', 'issue_label_removed'],
},
{
identifier: 'issue_label_removed',
name: 'Issue Label Removed',
type: 'label',
can_be_start_event: false,
allowed_end_events: [],
},
],
stages: [
{
name: 'issue',
legend: 'Related Issues',
description: 'Time before an issue gets scheduled',
id: 'issue',
position: 1,
hidden: false,
custom: false,
start_event_identifier: 'issue_created',
end_event_identifier: 'issue_stage_end',
},
{
name: 'plan',
legend: 'Related Issues',
description: 'Time before an issue starts implementation',
id: 'plan',
position: 2,
hidden: false,
custom: false,
start_event_identifier: 'plan_stage_start',
end_event_identifier: 'issue_first_mentioned_in_commit',
},
{
name: 'code',
legend: 'Related Merged Requests',
description: 'Time until first merge request',
id: 'code',
position: 3,
hidden: false,
custom: false,
start_event_identifier: 'code_stage_start',
end_event_identifier: 'merge_request_created',
},
{
name: 'test',
legend: 'Related Merged Requests',
description: 'Total test time for all commits/merges',
id: 'test',
position: 4,
hidden: false,
custom: false,
start_event_identifier: 'merge_request_last_build_started',
end_event_identifier: 'merge_request_last_build_finished',
},
{
name: 'review',
legend: 'Related Merged Requests',
description: 'Time between merge request creation and merge/close',
id: 'review',
position: 5,
hidden: false,
custom: false,
start_event_identifier: 'merge_request_created',
end_event_identifier: 'merge_request_merged',
},
{
name: 'staging',
legend: 'Related Merged Requests',
description: 'From merge request merge until deploy to production',
id: 'staging',
position: 6,
hidden: false,
custom: false,
start_event_identifier: 'merge_request_merged',
end_event_identifier: 'merge_request_first_deployed_to_production',
},
{
name: 'production',
legend: 'Related Merged Requests',
description: 'From issue creation until deploy to production',
id: 'production',
position: 7,
hidden: false,
custom: false,
start_event_identifier: 'merge_request_merged',
end_event_identifier: 'merge_request_first_deployed_to_production',
},
],
summary: [
{
value: 2,
title: 'New Issues',
},
{
value: 0,
title: 'Commits',
},
{
value: 0,
title: 'Deploys',
},
],
permissions: {
issue: true,
plan: true,
code: true,
test: true,
review: true,
staging: true,
production: true,
},
};
const rawEvents = [
{
name: 'Issue created',
identifier: 'issue_created',
type: 'simple',
can_be_start_event: true,
allowed_end_events: ['issue_closed', 'issue_merged'],
},
{
name: 'Merge request closed',
identifier: 'merge_request_closed',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: 'Issue closed',
identifier: 'issue_closed',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
{
name: 'Issue merged',
identifier: 'issue_merged',
type: 'simple',
can_be_start_event: false,
allowed_end_events: [],
},
];
export default {
apiResponse,
rawEvents,
};
import Vue from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
import CustomStageForm from '~/cycle_analytics/components/custom_stage_form.vue';
import mockData from './cycle_analytics.json';
const {
apiResponse: { events },
rawEvents,
} = mockData;
const startEvents = events.filter(ev => ev.can_be_start_event);
const stopEvents = events.filter(ev => !ev.can_be_start_event);
describe('CustomStageForm', () => {
function createComponent(props, shallow = true) {
const func = shallow ? shallowMount : mount;
return func(CustomStageForm, {
propsData: {
events,
...props,
},
sync: false, // fixes '$listeners is readonly' errors
});
}
let wrapper = null;
const sel = {
name: '[name="add-stage-name"]',
startEvent: '[name="add-stage-start-event"]',
stopEvent: '[name="add-stage-stop-event"]',
submit: '.js-add-stage',
};
function selectDropdownOption(_wrapper, dropdown, index) {
_wrapper
.find(dropdown)
.findAll('option')
.at(index)
.setSelected();
}
describe('Empty form', () => {
beforeEach(() => {
wrapper = createComponent({}, false);
});
describe.each([
['Name', sel.name, true],
['Start event', sel.startEvent, true],
['Stop event', sel.stopEvent, false],
['Submit', sel.submit, false],
])('by default', (field, $sel, enabledState) => {
const state = enabledState ? 'enabled' : 'disabled';
it(`field '${field}' is ${state}`, () => {
const el = wrapper.find($sel);
expect(el.exists()).toBe(true);
if (!enabledState) {
expect(el.attributes('disabled')).toBe('disabled');
} else {
expect(el.attributes('disabled')).toBeUndefined();
}
});
});
describe('Start event', () => {
describe('with events', () => {
beforeEach(() => {
wrapper = createComponent({}, false);
});
it('selects events with can_be_start_event=true for the start events dropdown', () => {
const select = wrapper.find(sel.startEvent);
startEvents.forEach(ev => {
expect(select.html()).toHaveHtml(
`<option value="${ev.identifier}">${ev.name}</option>`,
);
});
stopEvents.forEach(ev => {
expect(select.html()).not.toHaveHtml(
`<option value="${ev.identifier}">${ev.name}</option>`,
);
});
});
it('will exclude stop events for the dropdown', () => {
const select = wrapper.find(sel.startEvent);
stopEvents.forEach(ev => {
expect(select.html()).not.toHaveHtml(
`<option value="${ev.identifier}">${ev.name}</option>`,
);
});
});
});
describe.skip('start event label', () => {
it('is hidden by default', () => {});
it('will display the start event label field if a label event is selected', () => {});
});
});
describe('Stop event', () => {
beforeEach(() => {
wrapper = createComponent(
{
events: rawEvents,
},
false,
);
});
it('is enabled when a start event is selected', () => {
const el = wrapper.find(sel.stopEvent);
expect(el.attributes('disabled')).toBe('disabled');
const opts = wrapper.find(sel.startEvent).findAll('option');
opts.at(1).setSelected();
Vue.nextTick(() => expect(el.attributes('disabled')).toBeUndefined());
});
it('will update the list of stop events when a start event is changed', () => {
let stopOptions = wrapper.find(sel.stopEvent).findAll('option');
expect(stopOptions.length).toBe(1);
selectDropdownOption(wrapper, sel.startEvent, 1);
Vue.nextTick(() => {
stopOptions = wrapper.find(sel.stopEvent).findAll('option');
expect(stopOptions.length).toBe(3);
});
});
it('will only display valid stop events allowed for the selected start event', () => {
let stopOptions = wrapper.find(sel.stopEvent).findAll('option');
expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>');
selectDropdownOption(wrapper, sel.startEvent, 1);
Vue.nextTick(() => {
stopOptions = wrapper.find(sel.stopEvent).findAll('option');
[
{ name: 'Select stop event', identifier: '' },
{ name: 'Issue closed', identifier: 'issue_closed' },
{ name: 'Issue merged', identifier: 'issue_merged' },
].forEach(({ name, identifier }, i) => {
expect(stopOptions.at(i).html()).toEqual(
`<option value="${identifier}">${name}</option>`,
);
});
[
{ name: 'Issue created', identifier: 'issue_created' },
{ name: 'Merge request closed', identifier: 'merge_request_closed' },
].forEach(({ name, identifier }) => {
expect(wrapper.find(sel.stopEvent).html()).not.toHaveHtml(
`<option value="${identifier}">${name}</option>`,
);
});
});
});
describe.skip('Stop event label', () => {
it('is hidden by default', () => {});
it('will display the stop event label field if a label event is selected', () => {});
});
});
describe('Add stage button', () => {
beforeEach(() => {
wrapper = createComponent({}, false);
selectDropdownOption(wrapper, sel.startEvent, 1);
Vue.nextTick(() => {
selectDropdownOption(wrapper, sel.stopEvent, 1);
});
});
it('is enabled when all required fields are filled', () => {
const btn = wrapper.find(sel.submit);
expect(btn.attributes('disabled')).toBe('disabled');
wrapper.find(sel.name).setValue('Cool stage');
Vue.nextTick(() => {
expect(btn.attributes('disabled')).toBeUndefined();
});
});
describe('with all fields set', () => {
beforeEach(() => {
wrapper = createComponent({}, false);
selectDropdownOption(wrapper, sel.startEvent, 1);
Vue.nextTick(() => {
selectDropdownOption(wrapper, sel.stopEvent, 1);
wrapper.find(sel.name).setValue('Cool stage');
});
});
it('emits a `submit` event when clicked', () => {
const btn = wrapper.find(sel.submit);
expect(wrapper.emitted().submit).toBeUndefined();
btn.trigger('click');
expect(wrapper.emitted().submit).toBeTruthy();
expect(wrapper.emitted().submit.length).toBe(1);
});
it('`submit` event receives the latest data', () => {
const btn = wrapper.find(sel.submit);
expect(wrapper.emitted().submit).toBeUndefined();
const res = [
{
name: 'Cool stage',
startEvent: 'issue_created',
startEventLabel: '',
stopEvent: 'issue_stage_end',
stopEventLabel: '',
},
];
btn.trigger('click');
expect(wrapper.emitted().submit[0]).toEqual(res);
});
});
});
});
describe.skip('Prepopulated form', () => {
describe('Add stage button', () => {
it('is disabled by default', () => {});
it('is enabled when a field is changed and fields are valid', () => {});
it('emits a `submit` event when clicked', () => {});
});
});
});
{
"apiResponse": {
"events": [
{
"name": "Issue created",
"identifier": "issue_created",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["issue_stage_end"]
},
{
"name": "Issue first mentioned in a commit",
"identifier": "issue_first_mentioned_in_commit",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
},
{
"name": "Merge request created",
"identifier": "merge_request_created",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["merge_request_merged"]
},
{
"name": "Merge request first deployed to production",
"identifier": "merge_request_first_deployed_to_production",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
},
{
"name": "Merge request last build finish time",
"identifier": "merge_request_last_build_finished",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
},
{
"name": "Merge request last build start time",
"identifier": "merge_request_last_build_started",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["merge_request_last_build_finished"]
},
{
"name": "Merge request merged",
"identifier": "merge_request_merged",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["merge_request_first_deployed_to_production"]
},
{
"name": "Issue first mentioned in a commit",
"identifier": "code_stage_start",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["merge_request_created"]
},
{
"name": "Issue first associated with a milestone or issue first added to a board",
"identifier": "issue_stage_end",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
},
{
"name": "Issue first associated with a milestone or issue first added to a board",
"identifier": "plan_stage_start",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["issue_first_mentioned_in_commit"]
}
],
"stages": [
{
"name": "issue",
"legend": "Related Issues",
"description": "Time before an issue gets scheduled",
"id": "issue",
"position": 1,
"hidden": false,
"custom": false,
"start_event_identifier": "issue_created",
"end_event_identifier": "issue_stage_end"
},
{
"name": "plan",
"legend": "Related Issues",
"description": "Time before an issue starts implementation",
"id": "plan",
"position": 2,
"hidden": false,
"custom": false,
"start_event_identifier": "plan_stage_start",
"end_event_identifier": "issue_first_mentioned_in_commit"
},
{
"name": "code",
"legend": "Related Merged Requests",
"description": "Time until first merge request",
"id": "code",
"position": 3,
"hidden": false,
"custom": false,
"start_event_identifier": "code_stage_start",
"end_event_identifier": "merge_request_created"
},
{
"name": "test",
"legend": "Related Merged Requests",
"description": "Total test time for all commits/merges",
"id": "test",
"position": 4,
"hidden": false,
"custom": false,
"start_event_identifier": "merge_request_last_build_started",
"end_event_identifier": "merge_request_last_build_finished"
},
{
"name": "review",
"legend": "Related Merged Requests",
"description": "Time between merge request creation and merge/close",
"id": "review",
"position": 5,
"hidden": false,
"custom": false,
"start_event_identifier": "merge_request_created",
"end_event_identifier": "merge_request_merged"
},
{
"name": "staging",
"legend": "Related Merged Requests",
"description": "From merge request merge until deploy to production",
"id": "staging",
"position": 6,
"hidden": false,
"custom": false,
"start_event_identifier": "merge_request_merged",
"end_event_identifier": "merge_request_first_deployed_to_production"
},
{
"name": "production",
"legend": "Related Merged Requests",
"description": "From issue creation until deploy to production",
"id": "production",
"position": 7,
"hidden": false,
"custom": false,
"start_event_identifier": "merge_request_merged",
"end_event_identifier": "merge_request_first_deployed_to_production"
}
],
"summary": [
{
"value": 2,
"title": "New Issues"
},
{
"value": 0,
"title": "Commits"
},
{
"value": 0,
"title": "Deploys"
}
],
"permissions": {
"issue": true,
"plan": true,
"code": true,
"test": true,
"review": true,
"staging": true,
"production": true
}
},
"rawEvents": [
{
"name": "Issue created",
"identifier": "issue_created",
"type": "simple",
"can_be_start_event": true,
"allowed_end_events": ["issue_closed", "issue_merged"]
},
{
"name": "Merge request closed",
"identifier": "merge_request_closed",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
},
{
"name": "Issue closed",
"identifier": "issue_closed",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
},
{
"name": "Issue merged",
"identifier": "issue_merged",
"type": "simple",
"can_be_start_event": false,
"allowed_end_events": []
}
]
}
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