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 = { ...@@ -43,16 +43,6 @@ export const TASKS_BY_TYPE_FILTERS = {
LABEL: 'LABEL', 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 DEFAULT_VALUE_STREAM_ID = 'default';
export const OVERVIEW_METRICS = { 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 "" ...@@ -860,9 +860,6 @@ msgstr ""
msgid "%{strongStart}Deletes%{strongEnd} source branch" msgid "%{strongStart}Deletes%{strongEnd} source branch"
msgstr "" 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}" msgid "%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}"
msgstr "" msgstr ""
...@@ -9774,24 +9771,6 @@ msgstr "" ...@@ -9774,24 +9771,6 @@ msgstr ""
msgid "Custom range (UTC)" msgid "Custom range (UTC)"
msgstr "" 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" msgid "Customer Portal"
msgstr "" msgstr ""
...@@ -11902,9 +11881,6 @@ msgstr "" ...@@ -11902,9 +11881,6 @@ msgstr ""
msgid "Edit sidebar" msgid "Edit sidebar"
msgstr "" msgstr ""
msgid "Edit stage"
msgstr ""
msgid "Edit this file only." msgid "Edit this file only."
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