Commit 8aadce6f authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Luke Duncalfe

Provide default value stream configs

Provides the default values stream stage configs
from the backend via data attributes
parent 3b400367
# frozen_string_literal: true
module Analytics
module CycleAnalyticsHelper
def cycle_analytics_default_stage_config
Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params|
Analytics::CycleAnalytics::StagePresenter.new(stage_params)
end
end
end
end
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const NAME_MAX_LENGTH = 100; export const NAME_MAX_LENGTH = 100;
...@@ -32,6 +31,9 @@ export const I18N = { ...@@ -32,6 +31,9 @@ export const I18N = {
HIDDEN_DEFAULT_STAGE: s__('CreateValueStreamForm|%{name} (default)'), HIDDEN_DEFAULT_STAGE: s__('CreateValueStreamForm|%{name} (default)'),
TEMPLATE_DEFAULT: s__('CreateValueStreamForm|Create from default template'), TEMPLATE_DEFAULT: s__('CreateValueStreamForm|Create from default template'),
TEMPLATE_BLANK: s__('CreateValueStreamForm|Create from no template'), TEMPLATE_BLANK: s__('CreateValueStreamForm|Create from no template'),
ISSUE_STAGE_END: s__('CreateValueStreamForm|Issue stage end'),
PLAN_STAGE_START: s__('CreateValueStreamForm|Plan stage start'),
CODE_STAGE_START: s__('CreateValueStreamForm|Code stage start'),
}; };
export const ERRORS = { export const ERRORS = {
...@@ -73,51 +75,6 @@ export const defaultFields = { ...@@ -73,51 +75,6 @@ export const defaultFields = {
export const defaultCustomStageFields = { ...defaultFields, custom: true }; export const defaultCustomStageFields = { ...defaultFields, custom: true };
/**
* These stage configs are copied from the https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/cycle_analytics
* This is a stopgap solution and we should eventually provide these from an API endpoint
*
* More information: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49094#note_464116439
*/
const BASE_DEFAULT_STAGE_CONFIG = [
{
id: 'issue',
startEventIdentifier: ['issue_created'],
endEventIdentifier: ['issue_first_associated_with_milestone', 'issue_first_added_to_board'],
},
{
id: 'plan',
startEventIdentifier: ['issue_first_associated_with_milestone', 'issue_first_added_to_board'],
endEventIdentifier: ['issue_first_mentioned_in_commit'],
},
{
id: 'code',
startEventIdentifier: ['issue_first_mentioned_in_commit'],
endEventIdentifier: ['merge_request_created'],
},
{
id: 'test',
startEventIdentifier: ['merge_request_last_build_started'],
endEventIdentifier: ['merge_request_last_build_finished'],
},
{
id: 'review',
startEventIdentifier: ['merge_request_created'],
endEventIdentifier: ['merge_request_merged'],
},
{
id: 'staging',
startEventIdentifier: ['merge_request_merged'],
endEventIdentifier: ['merge_request_first_deployed_to_production'],
},
];
export const DEFAULT_STAGE_CONFIG = BASE_DEFAULT_STAGE_CONFIG.map(({ id, ...rest }) => ({
...rest,
custom: false,
name: capitalizeFirstCharacter(id),
}));
export const PRESET_OPTIONS_DEFAULT = 'default'; export const PRESET_OPTIONS_DEFAULT = 'default';
export const PRESET_OPTIONS_BLANK = 'blank'; export const PRESET_OPTIONS_BLANK = 'blank';
export const PRESET_OPTIONS = [ export const PRESET_OPTIONS = [
...@@ -130,3 +87,22 @@ export const PRESET_OPTIONS = [ ...@@ -130,3 +87,22 @@ export const PRESET_OPTIONS = [
value: PRESET_OPTIONS_BLANK, value: PRESET_OPTIONS_BLANK,
}, },
]; ];
// These events can only be set on the back end, they are used in the
// initial configuration of some default stages, but should not be
// selectable by users via the form, they are added here only for display
// purposes when we are editing a default value stream
export const ADDITIONAL_DEFAULT_STAGE_EVENTS = [
{
identifier: 'issue_stage_end',
name: I18N.ISSUE_STAGE_END,
},
{
identifier: 'plan_stage_start',
name: I18N.PLAN_STAGE_START,
},
{
identifier: 'code_stage_start',
name: I18N.CODE_STAGE_START,
},
];
<script> <script>
import { GlFormGroup, GlFormInput, GlFormText } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlFormText } from '@gitlab/ui';
import StageFieldActions from './stage_field_actions.vue'; import StageFieldActions from './stage_field_actions.vue';
import { I18N } from './constants'; import { I18N, ADDITIONAL_DEFAULT_STAGE_EVENTS } from './constants';
const findStageEvent = (stageEvents = [], eid = null) => { const findStageEvent = (stageEvents = [], eid = null) => {
if (!eid) return ''; if (!eid) return '';
return stageEvents.find(({ identifier }) => identifier === eid); return stageEvents.find(({ identifier }) => identifier === eid);
}; };
const eventIdsToName = (stageEvents = [], eventIds = []) => const eventIdToName = (stageEvents = [], eid) => {
eventIds const event = findStageEvent(stageEvents, eid);
.map((eid) => { return event?.name || '';
const stage = findStageEvent(stageEvents, eid); };
return stage?.name || '';
})
.join(', ');
export default { export default {
name: 'DefaultStageFields', name: 'DefaultStageFields',
...@@ -54,8 +51,8 @@ export default { ...@@ -54,8 +51,8 @@ export default {
renderError(field) { renderError(field) {
return this.errors[field] ? this.errors[field]?.join('\n') : null; return this.errors[field] ? this.errors[field]?.join('\n') : null;
}, },
eventName(eventIds = []) { eventName(eventId) {
return eventIdsToName(this.stageEvents, eventIds); return eventIdToName([...this.stageEvents, ...ADDITIONAL_DEFAULT_STAGE_EVENTS], eventId);
}, },
}, },
I18N, I18N,
......
...@@ -6,7 +6,6 @@ import { sprintf } from '~/locale'; ...@@ -6,7 +6,6 @@ import { sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { swapArrayItems } from '~/lib/utils/array_utility'; import { swapArrayItems } from '~/lib/utils/array_utility';
import { import {
DEFAULT_STAGE_CONFIG,
STAGE_SORT_DIRECTION, STAGE_SORT_DIRECTION,
I18N, I18N,
defaultCustomStageFields, defaultCustomStageFields,
...@@ -17,12 +16,12 @@ import { validateValueStreamName, validateStage } from './create_value_stream_fo ...@@ -17,12 +16,12 @@ import { validateValueStreamName, validateStage } from './create_value_stream_fo
import DefaultStageFields from './create_value_stream_form/default_stage_fields.vue'; import DefaultStageFields from './create_value_stream_form/default_stage_fields.vue';
import CustomStageFields from './create_value_stream_form/custom_stage_fields.vue'; import CustomStageFields from './create_value_stream_form/custom_stage_fields.vue';
const initializeStageErrors = (selectedPreset = PRESET_OPTIONS_DEFAULT) => const initializeStageErrors = (defaultStageConfig, selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT ? DEFAULT_STAGE_CONFIG.map(() => ({})) : [{}]; selectedPreset === PRESET_OPTIONS_DEFAULT ? defaultStageConfig.map(() => ({})) : [{}];
const initializeStages = (selectedPreset = PRESET_OPTIONS_DEFAULT) => const initializeStages = (defaultStageConfig, selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT selectedPreset === PRESET_OPTIONS_DEFAULT
? DEFAULT_STAGE_CONFIG ? defaultStageConfig
: [{ ...defaultCustomStageFields }]; : [{ ...defaultCustomStageFields }];
const formatStageDataForSubmission = (stages) => { const formatStageDataForSubmission = (stages) => {
...@@ -68,14 +67,24 @@ export default { ...@@ -68,14 +67,24 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
defaultStageConfig: {
type: Array,
required: true,
},
}, },
data() { data() {
const { hasExtendedFormFields, initialData, initialFormErrors, initialPreset } = this; const {
defaultStageConfig = [],
hasExtendedFormFields,
initialData,
initialFormErrors,
initialPreset,
} = this;
const { name: nameError = [], stages: stageErrors = [{}] } = initialFormErrors; const { name: nameError = [], stages: stageErrors = [{}] } = initialFormErrors;
const additionalFields = hasExtendedFormFields const additionalFields = hasExtendedFormFields
? { ? {
stages: initializeStages(initialPreset), stages: initializeStages(defaultStageConfig, initialPreset),
stageErrors: stageErrors || initializeStageErrors(initialPreset), stageErrors: stageErrors || initializeStageErrors(defaultStageConfig, initialPreset),
...initialData, ...initialData,
} }
: { stages: [], nameError }; : { stages: [], nameError };
...@@ -91,9 +100,7 @@ export default { ...@@ -91,9 +100,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState({ ...mapState({ isCreating: 'isCreatingValueStream' }),
isCreating: 'isCreatingValueStream',
}),
...mapState('customStages', ['formEvents']), ...mapState('customStages', ['formEvents']),
isValueStreamNameValid() { isValueStreamNameValid() {
return !this.nameError?.length; return !this.nameError?.length;
...@@ -151,8 +158,8 @@ export default { ...@@ -151,8 +158,8 @@ export default {
}); });
this.name = ''; this.name = '';
this.nameError = []; this.nameError = [];
this.stages = initializeStages(this.selectedPreset); this.stages = initializeStages(this.defaultStageConfig, this.selectedPreset);
this.stageErrors = initializeStageErrors(this.selectedPreset); this.stageErrors = initializeStageErrors(this.defaultStageConfig, this.selectedPreset);
} }
}); });
}, },
...@@ -222,7 +229,7 @@ export default { ...@@ -222,7 +229,7 @@ export default {
}, },
handleResetDefaults() { handleResetDefaults() {
this.name = ''; this.name = '';
DEFAULT_STAGE_CONFIG.forEach((stage, index) => { this.defaultStageConfig.forEach((stage, index) => {
Vue.set(this.stages, index, { ...stage, hidden: false }); Vue.set(this.stages, index, { ...stage, hidden: false });
}); });
}, },
...@@ -236,7 +243,11 @@ export default { ...@@ -236,7 +243,11 @@ export default {
} else { } else {
this.handleResetBlank(); this.handleResetBlank();
} }
Vue.set(this, 'stageErrors', initializeStageErrors(this.selectedPreset)); Vue.set(
this,
'stageErrors',
initializeStageErrors(this.defaultStageConfig, this.selectedPreset),
);
}, },
}, },
I18N, I18N,
......
...@@ -48,6 +48,7 @@ export default { ...@@ -48,6 +48,7 @@ export default {
data: 'valueStreams', data: 'valueStreams',
selectedValueStream: 'selectedValueStream', selectedValueStream: 'selectedValueStream',
initialFormErrors: 'createValueStreamErrors', initialFormErrors: 'createValueStreamErrors',
defaultStageConfig: 'defaultStageConfig',
}), }),
hasValueStreams() { hasValueStreams() {
return Boolean(this.data.length); return Boolean(this.data.length);
...@@ -127,6 +128,7 @@ export default { ...@@ -127,6 +128,7 @@ export default {
<value-stream-form <value-stream-form
:initial-form-errors="initialFormErrors" :initial-form-errors="initialFormErrors"
:has-extended-form-fields="hasExtendedFormFields" :has-extended-form-fields="hasExtendedFormFields"
:default-stage-config="defaultStageConfig"
/> />
<gl-modal <gl-modal
data-testid="delete-value-stream-modal" data-testid="delete-value-stream-modal"
......
...@@ -93,6 +93,7 @@ export default { ...@@ -93,6 +93,7 @@ export default {
createdBefore: endDate = null, createdBefore: endDate = null,
selectedProjects = [], selectedProjects = [],
selectedValueStream = {}, selectedValueStream = {},
defaultStageConfig = [],
} = {}, } = {},
) { ) {
state.isLoading = true; state.isLoading = true;
...@@ -101,6 +102,7 @@ export default { ...@@ -101,6 +102,7 @@ export default {
state.selectedValueStream = selectedValueStream; state.selectedValueStream = selectedValueStream;
state.startDate = startDate; state.startDate = startDate;
state.endDate = endDate; state.endDate = endDate;
state.defaultStageConfig = defaultStageConfig;
}, },
[types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS](state) { [types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS](state) {
state.isLoading = false; state.isLoading = false;
......
export default () => ({ export default () => ({
featureFlags: {}, featureFlags: {},
defaultStageConfig: [],
startDate: null, startDate: null,
endDate: null, endDate: null,
......
...@@ -21,6 +21,17 @@ export const buildValueStreamFromJson = (valueStream) => { ...@@ -21,6 +21,17 @@ export const buildValueStreamFromJson = (valueStream) => {
return id ? { id, name, isCustom } : null; return id ? { id, name, isCustom } : null;
}; };
/**
* Creates an array of stage objects from a json string. Returns an empty array if no stages are present.
*
* @param {String} stages - JSON encoded array of stages
* @returns {Array} - An array of stage objects
*/
const buildDefaultStagesFromJSON = (stages = '') => {
if (!stages.length) return [];
return JSON.parse(stages);
};
/** /**
* Creates a group object from a dataset. Returns null if no groupId is present. * Creates a group object from a dataset. Returns null if no groupId is present.
* *
...@@ -70,7 +81,7 @@ export const buildProjectFromDataset = (dataset) => { ...@@ -70,7 +81,7 @@ export const buildProjectFromDataset = (dataset) => {
* @param {String} data - JSON encoded array of projects * @param {String} data - JSON encoded array of projects
* @returns {Array} - An array of project objects * @returns {Array} - An array of project objects
*/ */
const buildProjectsFromJSON = (projects = []) => { const buildProjectsFromJSON = (projects = '') => {
if (!projects.length) return []; if (!projects.length) return [];
return JSON.parse(projects); return JSON.parse(projects);
}; };
...@@ -93,6 +104,7 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -93,6 +104,7 @@ export const buildCycleAnalyticsInitialData = ({
groupAvatarUrl = null, groupAvatarUrl = null,
labelsPath = '', labelsPath = '',
milestonesPath = '', milestonesPath = '',
defaultStages = null,
} = {}) => ({ } = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream), selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId group: groupId
...@@ -113,6 +125,9 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -113,6 +125,9 @@ export const buildCycleAnalyticsInitialData = ({
: [], : [],
labelsPath, labelsPath,
milestonesPath, milestonesPath,
defaultStageConfig: defaultStages
? buildDefaultStagesFromJSON(defaultStages).map(convertObjectPropsToCamelCase)
: [],
}); });
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => { export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {} - data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
- api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group) } : {} - api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group) } : {}
- image_paths = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg")} - image_paths = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg")}
- data_attributes.merge!(api_paths, image_paths) - default_stages = { default_stages: cycle_analytics_default_stage_config.to_json }
- data_attributes.merge!(api_paths, image_paths, default_stages)
- add_page_specific_style 'page_bundles/cycle_analytics' - add_page_specific_style 'page_bundles/cycle_analytics'
#js-cycle-analytics-app{ data: data_attributes } #js-cycle-analytics-app{ data: data_attributes }
...@@ -14,8 +14,8 @@ const ISSUE_CREATED = { id: 'issue_created', name: 'Issue created' }; ...@@ -14,8 +14,8 @@ const ISSUE_CREATED = { id: 'issue_created', name: 'Issue created' };
const ISSUE_CLOSED = { id: 'issue_closed', name: 'Issue closed' }; const ISSUE_CLOSED = { id: 'issue_closed', name: 'Issue closed' };
const defaultStage = { const defaultStage = {
name: 'Cool new stage', name: 'Cool new stage',
startEventIdentifier: [ISSUE_CREATED.id], startEventIdentifier: ISSUE_CREATED.id,
endEventIdentifier: [ISSUE_CLOSED.id], endEventIdentifier: ISSUE_CLOSED.id,
endEventLabel: 'some_label', endEventLabel: 'some_label',
}; };
......
...@@ -6,7 +6,7 @@ import DefaultStageFields from 'ee/analytics/cycle_analytics/components/create_v ...@@ -6,7 +6,7 @@ import DefaultStageFields from 'ee/analytics/cycle_analytics/components/create_v
import CustomStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/custom_stage_fields.vue'; import CustomStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/custom_stage_fields.vue';
import { PRESET_OPTIONS_BLANK } from 'ee/analytics/cycle_analytics/components/create_value_stream_form/constants'; import { PRESET_OPTIONS_BLANK } from 'ee/analytics/cycle_analytics/components/create_value_stream_form/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { customStageEvents as formEvents } from '../mock_data'; import { customStageEvents as formEvents, defaultStageConfig } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -28,11 +28,10 @@ describe('ValueStreamForm', () => { ...@@ -28,11 +28,10 @@ describe('ValueStreamForm', () => {
], ],
}; };
const fakeStore = ({ initialState = {} }) => const fakeStore = () =>
new Vuex.Store({ new Vuex.Store({
state: { state: {
isCreatingValueStream: false, isCreatingValueStream: false,
...initialState,
}, },
actions: { actions: {
createValueStream: createValueStreamMock, createValueStream: createValueStreamMock,
...@@ -47,17 +46,18 @@ describe('ValueStreamForm', () => { ...@@ -47,17 +46,18 @@ describe('ValueStreamForm', () => {
}, },
}); });
const createComponent = ({ props = {}, data = {}, initialState = {}, stubs = {} } = {}) => const createComponent = ({ props = {}, data = {}, stubs = {} } = {}) =>
extendedWrapper( extendedWrapper(
shallowMount(ValueStreamForm, { shallowMount(ValueStreamForm, {
localVue, localVue,
store: fakeStore({ initialState }), store: fakeStore(),
data() { data() {
return { return {
...data, ...data,
}; };
}, },
propsData: { propsData: {
defaultStageConfig,
...props, ...props,
}, },
mocks: { mocks: {
...@@ -132,11 +132,11 @@ describe('ValueStreamForm', () => { ...@@ -132,11 +132,11 @@ describe('ValueStreamForm', () => {
}); });
it('adds a blank custom stage when clicked', () => { it('adds a blank custom stage when clicked', () => {
expect(wrapper.vm.stages.length).toBe(6); expect(wrapper.vm.stages.length).toBe(defaultStageConfig.length);
clickAddStage(); clickAddStage();
expect(wrapper.vm.stages.length).toBe(7); expect(wrapper.vm.stages.length).toBe(defaultStageConfig.length + 1);
}); });
it('validates existing fields when clicked', () => { it('validates existing fields when clicked', () => {
......
...@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue'; import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import { findDropdownItemText } from '../helpers'; import { findDropdownItemText } from '../helpers';
import { valueStreams } from '../mock_data'; import { valueStreams, defaultStageConfig } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -28,6 +28,7 @@ describe('ValueStreamSelect', () => { ...@@ -28,6 +28,7 @@ describe('ValueStreamSelect', () => {
deleteValueStreamError: null, deleteValueStreamError: null,
valueStreams: [], valueStreams: [],
selectedValueStream: {}, selectedValueStream: {},
defaultStageConfig,
...initialState, ...initialState,
}, },
actions: { actions: {
......
...@@ -69,6 +69,30 @@ export const customizableStagesAndEvents = getJSONFixture( ...@@ -69,6 +69,30 @@ export const customizableStagesAndEvents = getJSONFixture(
const dummyState = {}; const dummyState = {};
export const defaultStageConfig = [
{
name: 'issue',
custom: false,
relativePosition: 1,
startEventIdentifier: 'issue_created',
endEventIdentifier: 'issue_stage_end',
},
{
name: 'plan',
custom: false,
relativePosition: 2,
startEventIdentifier: 'plan_stage_start',
endEventIdentifier: 'issue_first_mentioned_in_commit',
},
{
name: 'code',
custom: false,
relativePosition: 3,
startEventIdentifier: 'code_stage_start',
endEventIdentifier: 'merge_request_created',
},
];
// prepare the raw stage data for our components // prepare the raw stage data for our components
mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](dummyState, customizableStagesAndEvents.stages); mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](dummyState, customizableStagesAndEvents.stages);
......
...@@ -8562,6 +8562,9 @@ msgstr "" ...@@ -8562,6 +8562,9 @@ msgstr ""
msgid "CreateValueStreamForm|All default stages are currently visible" msgid "CreateValueStreamForm|All default stages are currently visible"
msgstr "" msgstr ""
msgid "CreateValueStreamForm|Code stage start"
msgstr ""
msgid "CreateValueStreamForm|Create from default template" msgid "CreateValueStreamForm|Create from default template"
msgstr "" msgstr ""
...@@ -8589,6 +8592,9 @@ msgstr "" ...@@ -8589,6 +8592,9 @@ msgstr ""
msgid "CreateValueStreamForm|Enter value stream name" msgid "CreateValueStreamForm|Enter value stream name"
msgstr "" msgstr ""
msgid "CreateValueStreamForm|Issue stage end"
msgstr ""
msgid "CreateValueStreamForm|Maximum length %{maxLength} characters" msgid "CreateValueStreamForm|Maximum length %{maxLength} characters"
msgstr "" msgstr ""
...@@ -8598,6 +8604,9 @@ msgstr "" ...@@ -8598,6 +8604,9 @@ msgstr ""
msgid "CreateValueStreamForm|New stage" msgid "CreateValueStreamForm|New stage"
msgstr "" msgstr ""
msgid "CreateValueStreamForm|Plan stage start"
msgstr ""
msgid "CreateValueStreamForm|Please select a start event first" msgid "CreateValueStreamForm|Please select a start event first"
msgstr "" 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