Commit 52e6c365 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '323984-remove-legacy-value-stream-form' into 'master'

Remove legacy custom value stream components and docs

See merge request gitlab-org/gitlab!61710
parents 0289efc2 2f83d038
<script>
export default {
props: {
active: {
type: Boolean,
required: true,
},
},
computed: {
activeClass() {
return 'active font-weight-bold border-style-solid border-color-blue-300';
},
inactiveClass() {
return 'bg-transparent border-style-dashed border-color-default';
},
},
};
</script>
<template>
<li
:class="[active ? activeClass : inactiveClass]"
class="js-add-stage-button stage-nav-item mb-1 rounded d-flex justify-content-center border-width-1px"
@click.prevent="$emit('showform')"
>
{{ s__('CustomCycleAnalytics|Add a stage') }}
</li>
</template>
<script>
import {
GlLoadingIcon,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSprintf,
GlButton,
} from '@gitlab/ui';
import { isEqual } from 'lodash';
import Vue from 'vue';
import { mapGetters, mapState } from 'vuex';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { STAGE_ACTIONS } from '../constants';
import { getAllowedEndEvents, getLabelEventsIdentifiers, isLabelEvent } from '../utils';
import { defaultFields, ERRORS, i18n } from './create_value_stream_form/constants';
import CustomStageFormFields from './create_value_stream_form/custom_stage_fields.vue';
import { validateStage, initializeFormData } from './create_value_stream_form/utils';
export default {
components: {
GlLoadingIcon,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSprintf,
GlButton,
CustomStageFormFields,
},
props: {
events: {
type: Array,
required: true,
},
},
data() {
return {
labelEvents: getLabelEventsIdentifiers(this.events),
fields: {},
errors: {},
};
},
computed: {
...mapGetters(['hiddenStages']),
...mapState('customStages', [
'isLoading',
'isSavingCustomStage',
'isEditingCustomStage',
'formInitialData',
'formErrors',
]),
hasErrors() {
return (
this.eventMismatchError || Object.values(this.errors).some((errArray) => errArray?.length)
);
},
startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
},
endEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
},
isComplete() {
if (this.hasErrors) {
return false;
}
const {
fields: {
name,
startEventIdentifier,
startEventLabelId,
endEventIdentifier,
endEventLabelId,
},
} = this;
const requiredFields = [startEventIdentifier, endEventIdentifier, name];
if (this.startEventRequiresLabel) {
requiredFields.push(startEventLabelId);
}
if (this.endEventRequiresLabel) {
requiredFields.push(endEventLabelId);
}
return requiredFields.every(
(fieldValue) => fieldValue && (fieldValue.length > 0 || fieldValue > 0),
);
},
isDirty() {
return !isEqual(this.fields, this.formInitialData || defaultFields);
},
eventMismatchError() {
const {
fields: { startEventIdentifier = null, endEventIdentifier = null },
} = this;
if (!startEventIdentifier || !endEventIdentifier) return true;
const endEvents = getAllowedEndEvents(this.events, startEventIdentifier);
return !endEvents.length || !endEvents.includes(endEventIdentifier);
},
saveStageText() {
return this.isEditingCustomStage ? i18n.BTN_UPDATE_STAGE : i18n.BTN_ADD_STAGE;
},
formTitle() {
return this.isEditingCustomStage ? i18n.TITLE_EDIT_STAGE : i18n.TITLE_ADD_STAGE;
},
hasHiddenStages() {
return this.hiddenStages.length;
},
},
watch: {
formInitialData(newFields = {}) {
this.fields = {
...defaultFields,
...newFields,
};
},
formErrors(newErrors = {}) {
this.errors = {
...newErrors,
};
},
},
mounted() {
this.resetFields();
},
methods: {
resetFields() {
const { formInitialData, formErrors } = this;
const { fields, errors } = initializeFormData({
fields: formInitialData,
errors: formErrors,
});
this.fields = { ...fields };
this.errors = { ...errors };
},
handleCancel() {
this.resetFields();
this.$emit('cancel');
},
handleSave() {
const data = convertObjectPropsToSnakeCase(this.fields);
if (this.isEditingCustomStage) {
const { id } = this.fields;
this.$emit(STAGE_ACTIONS.UPDATE, { ...data, id });
} else {
this.$emit(STAGE_ACTIONS.CREATE, data);
}
},
hasFieldErrors(key) {
return this.errors[key]?.length > 0;
},
fieldErrorMessage(key) {
return this.errors[key]?.join('\n');
},
handleRecoverStage(id) {
this.$emit(STAGE_ACTIONS.UPDATE, { id, hidden: false });
},
handleUpdateFields({ field, value }) {
this.fields = { ...this.fields, [field]: value };
const newErrors = validateStage({ ...this.fields, custom: true });
newErrors.endEventIdentifier =
this.fields.startEventIdentifier && this.eventMismatchError
? [ERRORS.INVALID_EVENT_PAIRS]
: newErrors.endEventIdentifier;
Vue.set(this, 'errors', newErrors);
},
},
i18n,
};
</script>
<template>
<div v-if="isLoading">
<gl-loading-icon class="mt-4" size="md" />
</div>
<form v-else class="custom-stage-form m-4 gl-mt-0">
<div class="gl-mb-1 gl-display-flex gl-justify-content-space-between gl-align-items-center">
<h4>{{ formTitle }}</h4>
<gl-dropdown
:text="$options.i18n.RECOVER_HIDDEN_STAGE"
data-testid="recover-hidden-stage-dropdown"
right
>
<gl-dropdown-section-header>{{
$options.i18n.RECOVER_STAGE_TITLE
}}</gl-dropdown-section-header>
<template v-if="hasHiddenStages">
<gl-dropdown-item
v-for="stage in hiddenStages"
:key="stage.id"
@click="handleRecoverStage(stage.id)"
>{{ stage.title }}</gl-dropdown-item
>
</template>
<p v-else class="gl-mx-5 gl-my-3">{{ $options.i18n.RECOVER_STAGES_VISIBLE }}</p>
</gl-dropdown>
</div>
<custom-stage-form-fields
:index="0"
:total-stages="1"
:stage="fields"
:errors="errors"
:stage-events="events"
@input="handleUpdateFields"
@select-label="({ field, value }) => handleUpdateFields({ field, value })"
/>
<div>
<gl-button
:disabled="!isDirty"
category="primary"
data-testid="cancel-custom-stage"
@click="handleCancel"
>
{{ $options.i18n.BTN_CANCEL }}
</gl-button>
<gl-button
:disabled="!isComplete || !isDirty"
variant="success"
category="primary"
data-testid="save-custom-stage"
@click="handleSave"
>
<gl-loading-icon v-if="isSavingCustomStage" size="sm" inline />
{{ saveStageText }}
</gl-button>
</div>
<div class="gl-mt-3">
<gl-sprintf
:message="
__(
'%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position.',
)
"
>
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</div>
</form>
</template>
......@@ -43,16 +43,6 @@ export const TASKS_BY_TYPE_FILTERS = {
LABEL: 'LABEL',
};
export const STAGE_ACTIONS = {
SELECT: 'selectStage',
EDIT: 'editStage',
REMOVE: 'removeStage',
HIDE: 'hideStage',
CREATE: 'createStage',
UPDATE: 'updateStage',
ADD_STAGE: 'showAddStageForm',
};
export const DEFAULT_VALUE_STREAM_ID = 'default';
export const OVERVIEW_METRICS = {
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Customizable Group Value Stream Analytics', :js do
include DragTo
include CycleAnalyticsHelpers
let_it_be(:group) { create(:group, name: 'CA-test-group') }
let_it_be(:sub_group) { create(:group, name: 'CA-sub-group', parent: group) }
let_it_be(:group2) { create(:group, name: 'CA-bad-test-group') }
let_it_be(:project) { create(:project, :repository, namespace: group, group: group, name: 'Cool fun project') }
let_it_be(:group_label1) { create(:group_label, group: group) }
let_it_be(:group_label2) { create(:group_label, group: group) }
let_it_be(:label) { create(:group_label, group: group2) }
let_it_be(:sub_group_label1) { create(:group_label, group: sub_group) }
let_it_be(:sub_group_label2) { create(:group_label, group: sub_group) }
let_it_be(:user) do
create(:user).tap do |u|
group.add_owner(u)
project.add_maintainer(u)
end
end
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
let(:stage_nav_selector) { '.stage-nav' }
let(:duration_stage_selector) { '.js-dropdown-stages' }
custom_stage_name = 'Cool beans'
custom_stage_with_labels_name = 'Cool beans - now with labels'
start_event_identifier = :merge_request_created
end_event_identifier = :merge_request_merged
start_event_text = "Merge request created"
end_event_text = "Merge request merged"
start_label_event = :issue_label_added
end_label_event = :issue_label_removed
start_event_field = 'custom-stage-start-event-0'
end_event_field = 'custom-stage-end-event-0'
start_field_label = 'custom-stage-start-event-label-0'
end_field_label = 'custom-stage-end-event-label-0'
name_field = 'custom-stage-name-0'
let(:add_stage_button) { '.js-add-stage-button' }
let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
let(:first_default_stage) { page.find('.stage-nav-item-cell', text: 'Issue').ancestor('.stage-nav-item') }
let(:first_custom_stage) { page.find('.stage-nav-item-cell', text: custom_stage_name).ancestor('.stage-nav-item') }
let(:nav) { page.find(stage_nav_selector) }
def create_custom_stage(parent_group = group)
Analytics::CycleAnalytics::Stages::CreateService.new(parent: parent_group, params: params, current_user: user).execute
end
def toggle_more_options(stage)
stage.hover
find_stage_actions_btn(stage).click
end
def select_dropdown_option(name, value = start_event_identifier)
toggle_dropdown name
page.find("[data-testid='#{name}'] .dropdown-menu").all('.dropdown-item').find { |item| item.value == value.to_s }.click
end
def select_dropdown_label(field, index = 1)
page.find("[data-testid='#{field}'] .dropdown-menu").all('.dropdown-item')[index].click
end
def drag_from_index_to_index(from, to)
drag_to(selector: '.stage-nav>ul',
from_index: from,
to_index: to)
end
def find_stage_actions_btn(stage)
stage.find('[data-testid="more-actions-toggle"]')
end
before do
stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true)
sign_in(user)
end
shared_examples 'submits custom stage form successfully' do |stage_name|
it 'custom stage is saved with confirmation message' do
fill_in name_field, with: stage_name
click_button(s_('CustomCycleAnalytics|Add stage'))
expect(page.find('.flash-notice')).to have_text(_("Your custom stage '%{title}' was created") % { title: stage_name })
expect(page).to have_selector('.stage-nav-item', text: stage_name)
end
end
shared_examples 'can create custom stages' do
context 'Custom stage form' do
let(:show_form_add_stage_button) { '.js-add-stage-button' }
before do
page.find(show_form_add_stage_button).click
wait_for_requests
end
context 'with empty fields' do
it 'submit button is disabled by default' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
end
context 'with all required fields set' do
before do
fill_in name_field, with: custom_stage_name
select_dropdown_option start_event_field, start_event_identifier
select_dropdown_option end_event_field, end_event_identifier
end
it 'does not have label dropdowns' do
expect(page).not_to have_content(s_('CustomCycleAnalytics|Start event label'))
expect(page).not_to have_content(s_('CustomCycleAnalytics|End event label'))
end
it 'submit button is disabled if a default name is used' do
fill_in name_field, with: 'issue'
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
it 'submit button is disabled if the start event changes' do
select_dropdown_option start_event_field, 'issue_created'
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
include_examples 'submits custom stage form successfully', custom_stage_name
end
context 'with label based stages selected' do
before do
fill_in name_field, with: custom_stage_with_labels_name
select_dropdown_option_by_value start_event_field, start_label_event
select_dropdown_option_by_value end_event_field, end_label_event
end
it 'submit button is disabled' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
context 'with labels available' do
it 'does not contain labels from outside the group' do
toggle_dropdown(start_field_label)
menu = page.find("[data-testid=#{start_field_label}] .dropdown-menu")
expect(menu).not_to have_content(other_label.name)
expect(menu).to have_content(first_label.name)
expect(menu).to have_content(second_label.name)
end
context 'with all required fields set' do
before do
toggle_dropdown(start_field_label)
select_dropdown_label start_field_label, 0
toggle_dropdown(end_field_label)
select_dropdown_label end_field_label, 1
end
include_examples 'submits custom stage form successfully', custom_stage_with_labels_name
end
end
end
end
end
shared_examples 'can edit custom stages' do
context 'Edit stage form' do
let(:stage_form_class) { '.custom-stage-form' }
let(:stage_save_button) { '[data-testid="save-custom-stage"]' }
let(:updated_custom_stage_name) { 'Extra uber cool stage' }
before do
toggle_more_options(first_custom_stage)
click_button(_('Edit stage'))
end
context 'with no changes to the data' do
it 'prepopulates the stage data and disables submit button' do
expect(page.find(stage_form_class)).to have_text(s_('CustomCycleAnalytics|Editing stage'))
expect(page.find("[name='#{name_field}']").value).to eq custom_stage_name
expect(page.find("[data-testid='#{start_event_field}']")).to have_text(start_event_text)
expect(page.find("[data-testid='#{end_event_field}']")).to have_text(end_event_text)
expect(page.find(stage_save_button)[:disabled]).to eq 'true'
end
end
context 'with changes' do
it 'persists updates to the stage' do
fill_in name_field, with: updated_custom_stage_name
page.find(stage_save_button).click
expect(page.find('.flash-notice')).to have_text(_('Stage data updated'))
expect(page.find(stage_nav_selector)).not_to have_text custom_stage_name
expect(page.find(stage_nav_selector)).to have_text updated_custom_stage_name
end
it 'disables the submit form button if incomplete' do
fill_in name_field, with: ''
expect(page.find(stage_save_button)[:disabled]).to eq 'true'
end
it 'doesnt update the stage if a default name is provided' do
fill_in name_field, with: 'issue'
page.find(stage_save_button).click
expect(page.find(stage_form_class)).to have_text(s_('CustomCycleAnalytics|Stage name already exists'))
end
end
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true displays a loading icon 1`] = `
"<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" data-testid=\\"save-custom-stage\\">
<gl-loading-icon-stub label=\\"Loading\\" size=\\"sm\\" color=\\"dark\\" inline=\\"true\\"></gl-loading-icon-stub>
Update stage
</gl-button-stub>"
`;
exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = `
"<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" data-testid=\\"save-custom-stage\\">
<gl-loading-icon-stub label=\\"Loading\\" size=\\"sm\\" color=\\"dark\\" inline=\\"true\\"></gl-loading-icon-stub>
Add stage
</gl-button-stub>"
`;
import { shallowMount } from '@vue/test-utils';
import AddStageButton from 'ee/analytics/cycle_analytics/components/add_stage_button.vue';
describe('AddStageButton', () => {
const active = false;
function createComponent(props) {
return shallowMount(AddStageButton, {
propsData: {
active,
...props,
},
});
}
let wrapper = null;
afterEach(() => {
wrapper.destroy();
});
describe('is not active', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('emits the `showform` event when clicked', () => {
wrapper = createComponent();
expect(wrapper.emitted().showform).toBeUndefined();
wrapper.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().showform).toHaveLength(1);
});
});
it('does not have the active class', () => {
expect(wrapper.classes('active')).toBe(false);
});
});
describe('is active', () => {
it('has the active class when active=true', () => {
wrapper = createComponent({ active: true });
expect(wrapper.classes('active')).toBe(true);
});
});
});
......@@ -860,9 +860,6 @@ msgstr ""
msgid "%{strongStart}Deletes%{strongEnd} source branch"
msgstr ""
msgid "%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position."
msgstr ""
msgid "%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}"
msgstr ""
......@@ -9774,24 +9771,6 @@ msgstr ""
msgid "Custom range (UTC)"
msgstr ""
msgid "CustomCycleAnalytics|Add a stage"
msgstr ""
msgid "CustomCycleAnalytics|Add stage"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage"
msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
msgid "CustomCycleAnalytics|Start event label"
msgstr ""
msgid "Customer Portal"
msgstr ""
......@@ -11902,9 +11881,6 @@ msgstr ""
msgid "Edit sidebar"
msgstr ""
msgid "Edit stage"
msgstr ""
msgid "Edit this file only."
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