Commit b4758cfa authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

FE Edit custom stage

Added vuex actions, mutations and state to
edit custom cycle analytics stages. Additionally
the isAddingStage state was split into
isCreatingStage and isEditingStage to reflect the
states of the form
parent 357b9ec5
......@@ -55,8 +55,9 @@ export default {
'isLoadingChartData',
'isLoadingDurationChart',
'isEmptyStage',
'isAddingCustomStage',
'isSavingCustomStage',
'isCreatingCustomStage',
'isEditingCustomStage',
'selectedGroup',
'selectedStageId',
'stages',
......@@ -68,6 +69,7 @@ export default {
'startDate',
'endDate',
'tasksByType',
'customStageFormInitData',
]),
...mapGetters([
'currentStage',
......@@ -117,11 +119,12 @@ export default {
'showCustomStageForm',
'setDateRange',
'fetchTasksByTypeData',
'updateSelectedDurationChartStages',
'createCustomStage',
'updateStage',
'removeStage',
'updateSelectedDurationChartStages',
'setFeatureFlags',
'editCustomStage',
]),
onGroupSelect(group) {
this.setSelectedGroup(group);
......@@ -140,6 +143,9 @@ export default {
onShowAddStageForm() {
this.showCustomStageForm();
},
onShowEditStageForm(initData = {}) {
this.editCustomStage(initData);
},
initDateRange() {
const endDate = new Date(Date.now());
const startDate = getDateInPast(endDate, DEFAULT_DAYS_IN_PAST);
......@@ -243,15 +249,18 @@ export default {
:stages="stages"
:is-loading="isLoadingStage"
:is-empty-stage="isEmptyStage"
:is-adding-custom-stage="isAddingCustomStage"
:is-saving-custom-stage="isSavingCustomStage"
:is-creating-custom-stage="isCreatingCustomStage"
:is-editing-custom-stage="isEditingCustomStage"
:current-stage-events="currentStageEvents"
:custom-stage-form-events="customStageFormEvents"
:labels="labels"
:no-data-svg-path="noDataSvgPath"
:no-access-svg-path="noAccessSvgPath"
:can-edit-stages="hasCustomizableCycleAnalytics"
:custom-stage-form-init-data="customStageFormInitData"
@selectStage="onStageSelect"
@editStage="onShowEditStageForm"
@showAddStageForm="onShowAddStageForm"
@submit="onCreateCustomStage"
@hideStage="onUpdateStage"
......
......@@ -2,7 +2,9 @@
import { isEqual } from 'underscore';
import { GlButton, GlFormGroup, GlFormInput, GlFormSelect, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import LabelsSelector from './labels_selector.vue';
import { STAGE_ACTIONS } from '../constants';
import {
isStartEvent,
isLabelEvent,
......@@ -13,13 +15,20 @@ import {
} from '../utils';
const initFields = {
name: '',
startEvent: '',
startEventLabel: null,
stopEvent: '',
stopEventLabel: null,
name: null,
startEventIdentifier: null,
startEventLabelId: null,
endEventIdentifier: null,
endEventLabelId: null,
};
// TODO: should be a util / use a util if exists...
const snakeFields = (fields = {}) =>
Object.entries(fields).reduce((acc, curr) => {
const [key, value] = curr;
return { ...acc, [convertToSnakeCase(key)]: value };
}, {});
export default {
components: {
GlButton,
......@@ -50,11 +59,16 @@ export default {
required: false,
default: false,
},
isEditingCustomStage: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
fields: {
...initFields,
...this.initialFields,
},
};
},
......@@ -65,30 +79,34 @@ export default {
...this.events.filter(isStartEvent).map(eventToOption),
];
},
stopEventOptions() {
const stopEvents = getAllowedEndEvents(this.events, this.fields.startEvent);
endEventOptions() {
const endEvents = getAllowedEndEvents(this.events, this.fields.startEventIdentifier);
return [
{ value: null, text: s__('CustomCycleAnalytics|Select stop event') },
...eventsByIdentifier(this.events, stopEvents).map(eventToOption),
...eventsByIdentifier(this.events, endEvents).map(eventToOption),
];
},
hasStartEvent() {
return this.fields.startEvent;
return this.fields.startEventIdentifier;
},
startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEvent);
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
},
stopEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.stopEvent);
endEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
},
isComplete() {
if (!this.hasValidStartAndStopEventPair) return false;
const requiredFields = [this.fields.startEvent, this.fields.stopEvent, this.fields.name];
if (!this.hasValidStartAndEndEventPair) return false;
const requiredFields = [
this.fields.startEventIdentifier,
this.fields.endEventIdentifier,
this.fields.name,
];
if (this.startEventRequiresLabel) {
requiredFields.push(this.fields.startEventLabel);
requiredFields.push(this.fields.startEventLabelId);
}
if (this.stopEventRequiresLabel) {
requiredFields.push(this.fields.stopEventLabel);
if (this.endEventRequiresLabel) {
requiredFields.push(this.fields.endEventLabelId);
}
return requiredFields.every(
fieldValue => fieldValue && (fieldValue.length > 0 || fieldValue > 0),
......@@ -97,21 +115,31 @@ export default {
isDirty() {
return !isEqual(this.initialFields, this.fields);
},
hasValidStartAndStopEventPair() {
hasValidStartAndEndEventPair() {
const {
fields: { startEvent, stopEvent },
fields: { startEventIdentifier, endEventIdentifier },
} = this;
if (startEvent && stopEvent) {
const stopEvents = getAllowedEndEvents(this.events, startEvent);
return stopEvents.length && stopEvents.includes(stopEvent);
if (startEventIdentifier && endEventIdentifier) {
const endEvents = getAllowedEndEvents(this.events, startEventIdentifier);
return endEvents.length && endEvents.includes(endEventIdentifier);
}
return true;
},
stopEventError() {
return !this.hasValidStartAndStopEventPair
endEventError() {
return !this.hasValidStartAndEndEventPair
? s__('CustomCycleAnalytics|Start event changed, please select a valid stop event')
: null;
},
saveStageText() {
return this.isEditingCustomStage
? s__('CustomCycleAnalytics|Edit stage')
: s__('CustomCycleAnalytics|Add stage');
},
formTitle() {
return this.isEditingCustomStage
? s__('CustomCycleAnalytics|Editing stage')
: s__('CustomCycleAnalytics|New stage');
},
},
mounted() {
this.labelEvents = getLabelEventsIdentifiers(this.events);
......@@ -122,14 +150,10 @@ export default {
this.$emit('cancel');
},
handleSave() {
const { startEvent, startEventLabel, stopEvent, stopEventLabel, name } = this.fields;
this.$emit('submit', {
name,
start_event_identifier: startEvent,
start_event_label_id: startEventLabel,
end_event_identifier: stopEvent,
end_event_label_id: stopEventLabel,
});
this.$emit(
this.isEditingCustomStage ? STAGE_ACTIONS.EDIT : STAGE_ACTIONS.SAVE,
snakeFields(this.fields),
);
},
handleSelectLabel(key, labelId = null) {
this.fields[key] = labelId;
......@@ -143,7 +167,7 @@ export default {
<template>
<form class="custom-stage-form m-4 mt-0">
<div class="mb-1">
<h4>{{ s__('CustomCycleAnalytics|New stage') }}</h4>
<h4>{{ formTitle }}</h4>
</div>
<gl-form-group :label="s__('CustomCycleAnalytics|Name')">
<gl-form-input
......@@ -159,8 +183,8 @@ export default {
<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="custom-stage-start-event"
v-model="fields.startEventIdentifier"
name="add-stage-start-event"
:required="true"
:options="startEventOptions"
/>
......@@ -170,41 +194,41 @@ export default {
<gl-form-group :label="s__('CustomCycleAnalytics|Start event label')">
<labels-selector
:labels="labels"
:selected-label-id="fields.startEventLabel"
name="custom-stage-start-event-label"
@selectLabel="labelId => handleSelectLabel('startEventLabel', labelId)"
@clearLabel="handleClearLabel('startEventLabel')"
:selected-label-id="fields.startEventLabelId"
name="add-stage-start-event-label"
@selectLabel="labelId => handleSelectLabel('startEventLabelId', labelId)"
@clearLabel="handleClearLabel('startEventLabelId')"
/>
</gl-form-group>
</div>
</div>
<div class="d-flex" :class="{ 'justify-content-between': stopEventRequiresLabel }">
<div :class="[stopEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<div class="d-flex" :class="{ 'justify-content-between': endEventRequiresLabel }">
<div :class="[endEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group
:label="s__('CustomCycleAnalytics|Stop event')"
:description="
!hasStartEvent ? s__('CustomCycleAnalytics|Please select a start event first') : ''
"
:state="hasValidStartAndStopEventPair"
:invalid-feedback="stopEventError"
:state="hasValidStartAndEndEventPair"
:invalid-feedback="endEventError"
>
<gl-form-select
v-model="fields.stopEvent"
name="custom-stage-stop-event"
:options="stopEventOptions"
v-model="fields.endEventIdentifier"
name="add-stage-stop-event"
:options="endEventOptions"
:required="true"
:disabled="!hasStartEvent"
/>
</gl-form-group>
</div>
<div v-if="stopEventRequiresLabel" class="w-50 ml-1">
<div v-if="endEventRequiresLabel" class="w-50 ml-1">
<gl-form-group :label="s__('CustomCycleAnalytics|Stop event label')">
<labels-selector
:labels="labels"
:selected-label-id="fields.stopEventLabel"
name="custom-stage-stop-event-label"
@selectLabel="labelId => handleSelectLabel('stopEventLabel', labelId)"
@clearLabel="handleClearLabel('stopEventLabel')"
:selected-label-id="fields.endEventLabelId"
name="add-stage-stop-event-label"
@selectLabel="labelId => handleSelectLabel('endEventLabelId', labelId)"
@clearLabel="handleClearLabel('endEventLabelId')"
/>
</gl-form-group>
</div>
......@@ -213,7 +237,7 @@ export default {
<div class="custom-stage-form-actions">
<button
:disabled="!isDirty"
class="btn btn-cancel js-custom-stage-form-cancel"
class="btn btn-cancel js-save-stage-cancel"
type="button"
@click="handleCancel"
>
......@@ -222,11 +246,11 @@ export default {
<button
:disabled="!isComplete || !isDirty"
type="button"
class="js-custom-stage-form-submit btn btn-success"
class="js-save-stage btn btn-success"
@click="handleSave"
>
<gl-loading-icon v-if="isSavingCustomStage" size="sm" inline />
{{ s__('CustomCycleAnalytics|Add stage') }}
{{ saveStageText }}
</button>
</div>
</form>
......
......@@ -41,7 +41,11 @@ export default {
type: Boolean,
required: true,
},
isAddingCustomStage: {
isCreatingCustomStage: {
type: Boolean,
required: true,
},
isEditingCustomStage: {
type: Boolean,
required: true,
},
......@@ -73,6 +77,11 @@ export default {
type: Boolean,
required: true,
},
customStageFormInitData: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
stageName() {
......@@ -147,12 +156,13 @@ export default {
:key="`ca-stage-title-${stage.title}`"
:title="stage.title"
:value="stage.value"
:is-active="!isAddingCustomStage && stage.id === currentStage.id"
:is-active="!isCreatingCustomStage && stage.id === currentStage.id"
:can-edit="canEditStages"
:is-default-stage="!stage.custom"
@select="$emit('selectStage', stage)"
@remove="removeStage(stage.id)"
@hide="hideStage(stage.id)"
@edit="$emit(STAGE_ACTIONS.EDIT, stageData)"
/>
<add-stage-button
v-if="canEditStages"
......@@ -164,11 +174,14 @@ export default {
<div class="section stage-events">
<gl-loading-icon v-if="isLoading" class="mt-4" size="md" />
<custom-stage-form
v-else-if="isAddingCustomStage"
v-else-if="isCreatingCustomStage || isEditingCustomStage"
:events="customStageFormEvents"
:labels="labels"
:is-saving-custom-stage="isSavingCustomStage"
:initial-fields="customStageFormInitData"
:is-editing-custom-stage="isEditingCustomStage"
@submit="$emit('submit', $event)"
@saveStage="saveStage"
/>
<template v-else>
<stage-event-list
......
......@@ -34,6 +34,7 @@ export const TASKS_BY_TYPE_SUBJECT_ISSUE = 'Issue';
export const TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST = 'MergeRequest';
export const STAGE_ACTIONS = {
SELECT: 'selectStage',
EDIT: 'editStage',
REMOVE: 'removeStage',
SAVE: 'saveStage',
......
......@@ -83,6 +83,16 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
.catch(error => dispatch('receiveCycleAnalyticsDataError', error));
};
export const hideCustomStageForm = ({ commit }) => commit(types.HIDE_CUSTOM_STAGE_FORM);
export const showCustomStageForm = ({ commit }) => commit(types.SHOW_CUSTOM_STAGE_FORM);
export const editCustomStage = ({ commit, dispatch }, initData = {}) => {
commit(types.EDIT_CUSTOM_STAGE, initData);
if (initData.id) {
dispatch('setSelectedStageId', initData.id);
}
};
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
export const receiveSummaryDataError = ({ commit }, error) => {
......@@ -112,9 +122,6 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => {
export const requestGroupStagesAndEvents = ({ commit }) =>
commit(types.REQUEST_GROUP_STAGES_AND_EVENTS);
export const hideCustomStageForm = ({ commit }) => commit(types.HIDE_CUSTOM_STAGE_FORM);
export const showCustomStageForm = ({ commit }) => commit(types.SHOW_CUSTOM_STAGE_FORM);
export const receiveGroupLabelsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_GROUP_LABELS_SUCCESS, data);
......@@ -197,7 +204,6 @@ export const createCustomStage = ({ dispatch, state }, data) => {
const {
selectedGroup: { fullPath },
} = state;
dispatch('requestCreateCustomStage');
return Api.cycleAnalyticsCreateStage(fullPath, data)
......@@ -249,7 +255,7 @@ export const receiveUpdateStageSuccess = ({ commit, dispatch }) => {
commit(types.RECEIVE_UPDATE_STAGE_RESPONSE);
createFlash(__(`Stage data updated`), 'notice');
dispatch('fetchCycleAnalyticsData');
dispatch('fetchGroupStagesAndEvents');
};
export const receiveUpdateStageError = ({ commit }) => {
......
......@@ -18,6 +18,7 @@ export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
export const HIDE_CUSTOM_STAGE_FORM = 'HIDE_CUSTOM_STAGE_FORM';
export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM';
export const EDIT_CUSTOM_STAGE = 'EDIT_CUSTOM_STAGE';
export const REQUEST_GROUP_LABELS = 'REQUEST_GROUP_LABELS';
export const RECEIVE_GROUP_LABELS_SUCCESS = 'RECEIVE_GROUP_LABELS_SUCCESS';
......
......@@ -25,7 +25,8 @@ export default {
},
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.isAddingCustomStage = false;
state.isCreatingCustomStage = false;
state.isEditingCustomStage = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state) {
state.errorCode = null;
......@@ -75,8 +76,34 @@ export default {
labelIds: [],
};
},
[types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isCreatingCustomStage = true;
state.customStageFormInitData = {};
},
[types.EDIT_CUSTOM_STAGE](state, initData) {
console.log('EDIT_CUSTOM_STAGE::initData', initData);
const {
title: name,
startEventIdentifier,
endEventIdentifier,
startEventLabelId,
endEventLabelId,
} = initData;
state.isEditingCustomStage = true;
state.customStageFormInitData = {
...state.customStageFormInitData,
name,
startEventIdentifier,
endEventIdentifier,
startEventLabelId,
endEventLabelId,
};
},
[types.HIDE_CUSTOM_STAGE_FORM](state) {
state.isAddingCustomStage = false;
state.isEditingCustomStage = false;
state.isCreatingCustomStage = false;
state.customStageFormInitData = {};
},
[types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isAddingCustomStage = true;
......
......@@ -14,8 +14,9 @@ export default () => ({
isEmptyStage: false,
errorCode: null,
isAddingCustomStage: false,
isSavingCustomStage: false,
isCreatingCustomStage: false,
isEditingCustomStage: false,
selectedGroup: null,
selectedProjectIds: [],
......
......@@ -36,8 +36,9 @@ function createComponent(props = {}, shallow = false) {
isLoading: false,
isLoadingSummaryData: false,
isEmptyStage: false,
isAddingCustomStage: false,
isSavingCustomStage: false,
isCreatingCustomStage: false,
isEditingCustomStage: false,
noDataSvgPath,
noAccessSvgPath,
canEditStages: false,
......
......@@ -33,8 +33,9 @@ describe('Cycle analytics mutations', () => {
it.each`
mutation | stateKey | value
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${false}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${true}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${true}
${types.EDIT_CUSTOM_STAGE} | ${'isEditingCustomStage'} | ${true}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
......
......@@ -5196,6 +5196,12 @@ msgstr ""
msgid "CustomCycleAnalytics|Add stage"
msgstr ""
msgid "CustomCycleAnalytics|Edit stage"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage"
msgstr ""
msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr ""
......
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