Commit b4108e90 authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch 'ek-use-be-default-value-stream-stages' into 'master'

Expose default value stream configuration

See merge request gitlab-org/gitlab!53688
parents 42d0e21e 8aadce6f
# 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