Commit f7121c45 authored by David O'Regan's avatar David O'Regan

Merge branch '268362-edit-escalation-policy' into 'master'

Edit escalation policy

See merge request gitlab-org/gitlab!63232
parents 23dc9dff f787fe51
<script>
import { GlLink, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { cloneDeep, uniqueId } from 'lodash';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { DEFAULT_ESCALATION_RULE } from '../constants';
import { DEFAULT_ACTION, DEFAULT_ESCALATION_RULE } from '../constants';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import EscalationRule from './escalation_rule.vue';
......@@ -48,7 +48,6 @@ export default {
return {
schedules: [],
rules: [],
uid: 0,
};
},
apollo: {
......@@ -74,11 +73,29 @@ export default {
},
},
mounted() {
this.addRule();
this.rules = this.form.rules.map((rule) => {
const {
status,
elapsedTimeSeconds,
oncallSchedule: { iid: oncallScheduleIid },
} = rule;
return {
status,
elapsedTimeSeconds,
action: DEFAULT_ACTION,
oncallScheduleIid,
key: uniqueId(),
};
});
if (!this.rules.length) {
this.addRule();
}
},
methods: {
addRule() {
this.rules.push({ ...cloneDeep(DEFAULT_ESCALATION_RULE), key: this.getUid() });
this.rules.push({ ...cloneDeep(DEFAULT_ESCALATION_RULE), key: uniqueId() });
},
updateEscalationRules(index, rule) {
this.rules[index] = { ...this.rules[index], ...rule };
......@@ -91,10 +108,6 @@ export default {
emitRulesUpdate() {
this.$emit('update-escalation-policy-form', { field: 'rules', value: this.rules });
},
getUid() {
this.uid += 1;
return this.uid;
},
},
};
</script>
......
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { set } from 'lodash';
import { set, isEqual } from 'lodash';
import { s__, __ } from '~/locale';
import { addEscalationPolicyModalId } from '../constants';
import { updateStoreOnEscalationPolicyCreate } from '../graphql/cache_updates';
import {
updateStoreOnEscalationPolicyCreate,
updateStoreOnEscalationPolicyUpdate,
} from '../graphql/cache_updates';
import createEscalationPolicyMutation from '../graphql/mutations/create_escalation_policy.mutation.graphql';
import updateEscalationPolicyMutation from '../graphql/mutations/update_escalation_policy.mutation.graphql';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
import { isNameFieldValid, getRulesValidationState } from '../utils';
import AddEditEscalationPolicyForm from './add_edit_escalation_policy_form.vue';
......@@ -17,7 +20,6 @@ export const i18n = {
export default {
i18n,
addEscalationPolicyModalId,
components: {
GlModal,
GlAlert,
......@@ -30,15 +32,21 @@ export default {
required: false,
default: () => ({}),
},
isEditMode: {
type: Boolean,
required: false,
default: false,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
form: {
name: this.escalationPolicy.name,
description: this.escalationPolicy.description,
rules: [],
},
form: this.getInitialState(),
initialState: this.getInitialState(),
validationState: {
name: null,
rules: [],
......@@ -47,14 +55,17 @@ export default {
};
},
computed: {
title() {
return this.isEditMode ? i18n.editEscalationPolicy : i18n.addEscalationPolicy;
},
actionsProps() {
return {
primary: {
text: i18n.addEscalationPolicy,
text: this.title,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
{ disabled: !this.isFormValid },
{ disabled: !this.isFormValid || !this.isFormDirty },
],
},
cancel: {
......@@ -65,13 +76,32 @@ export default {
isFormValid() {
return (
this.validationState.name &&
(this.isEditMode ? true : this.validationState.rules.length) &&
this.validationState.rules.every(
({ isTimeValid, isScheduleValid }) => isTimeValid && isScheduleValid,
)
);
},
isFormDirty() {
return (
this.form.name !== this.initialState.name ||
this.form.description !== this.initialState.description ||
!isEqual(this.getRules(this.form.rules), this.getRules(this.initialState.rules))
);
},
requestParams() {
const id = this.isEditMode ? { id: this.escalationPolicy.id } : {};
return { ...this.form, ...id, rules: this.getRules(this.form.rules) };
},
},
methods: {
getInitialState() {
return {
name: this.escalationPolicy.name ?? '',
description: this.escalationPolicy.description ?? '',
rules: this.escalationPolicy.rules ?? [],
};
},
updateForm({ field, value }) {
set(this.form, field, value);
this.validateForm(field);
......@@ -85,7 +115,7 @@ export default {
variables: {
input: {
projectPath,
...this.getRequestParams(),
...this.requestParams,
},
},
update(store, { data }) {
......@@ -117,14 +147,51 @@ export default {
this.loading = false;
});
},
getRequestParams() {
const rules = this.form.rules.map(({ status, elapsedTimeSeconds, oncallScheduleIid }) => ({
status,
elapsedTimeSeconds,
oncallScheduleIid,
}));
return { ...this.form, rules };
updateEscalationPolicy() {
this.loading = true;
const { projectPath } = this;
this.$apollo
.mutate({
mutation: updateEscalationPolicyMutation,
variables: {
input: this.requestParams,
},
update(store, { data }) {
updateStoreOnEscalationPolicyUpdate(store, getEscalationPoliciesQuery, data, {
projectPath,
});
},
})
.then(
({
data: {
escalationPolicyUpdate: {
errors: [error],
},
},
}) => {
if (error) {
throw error;
}
this.$refs.addUpdateEscalationPolicyModal.hide();
this.resetForm();
},
)
.catch((error) => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
getRules(rules) {
return rules.map(
({ status, elapsedTimeSeconds, oncallScheduleIid, oncallSchedule: { iid } = {} }) => ({
status,
elapsedTimeSeconds,
oncallScheduleIid: oncallScheduleIid || iid,
}),
);
},
validateForm(field) {
if (field === 'name') {
......@@ -138,11 +205,21 @@ export default {
this.error = null;
},
resetForm() {
this.form = {
name: '',
description: '',
rules: [],
};
if (this.isEditMode) {
const { name, description, rules } = this.escalationPolicy;
this.form = {
name,
description,
rules,
};
} else {
this.form = {
name: '',
description: '',
rules: [],
};
}
this.validationState = {
name: null,
rules: [],
......@@ -157,11 +234,11 @@ export default {
<gl-modal
ref="addUpdateEscalationPolicyModal"
class="escalation-policy-modal"
:modal-id="$options.addEscalationPolicyModalId"
:title="$options.i18n.addEscalationPolicy"
:modal-id="modalId"
:title="title"
:action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel"
@primary.prevent="createEscalationPolicy"
@primary.prevent="isEditMode ? updateEscalationPolicy() : createEscalationPolicy()"
@canceled="resetForm"
@close="resetForm"
>
......
......@@ -72,15 +72,6 @@ export default {
<template v-else-if="hasPolicies">
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
<h2>{{ $options.i18n.title }}</h2>
<gl-button
v-gl-modal="$options.addEscalationPolicyModalId"
:title="$options.i18n.addPolicy"
category="secondary"
variant="confirm"
class="gl-mt-5"
>
{{ $options.i18n.addPolicy }}
</gl-button>
</div>
<escalation-policy
v-for="(policy, index) in escalationPolicies"
......
......@@ -15,7 +15,9 @@ import {
ALERT_STATUSES,
DEFAULT_ACTION,
deleteEscalationPolicyModalId,
editEscalationPolicyModalId,
} from '../constants';
import EditEscalationPolicyModal from './add_edit_escalation_policy_modal.vue';
import DeleteEscalationPolicyModal from './delete_escalation_policy_modal.vue';
export const i18n = {
......@@ -45,6 +47,7 @@ export default {
GlIcon,
GlCollapse,
DeleteEscalationPolicyModal,
EditEscalationPolicyModal,
},
directives: {
GlModal: GlModalDirective,
......@@ -75,6 +78,9 @@ export default {
policyVisibleAngleIconLabel() {
return this.isPolicyVisible ? __('Collapse') : __('Expand');
},
editPolicyModalId() {
return `${editEscalationPolicyModalId}-${this.policy.id}`;
},
deletePolicyModalId() {
return `${deleteEscalationPolicyModalId}-${this.policy.id}`;
},
......@@ -106,11 +112,11 @@ export default {
<h3 class="gl-font-weight-bold gl-font-lg gl-m-0">{{ policy.name }}</h3>
<gl-button-group class="gl-ml-auto">
<gl-button
v-gl-modal="editPolicyModalId"
v-gl-tooltip
:title="$options.i18n.editPolicy"
icon="pencil"
:aria-label="$options.i18n.editPolicy"
disabled
/>
<gl-button
v-gl-modal="deletePolicyModalId"
......@@ -163,5 +169,10 @@ export default {
</gl-card>
<delete-escalation-policy-modal :escalation-policy="policy" :modal-id="deletePolicyModalId" />
<edit-escalation-policy-modal
:escalation-policy="policy"
:modal-id="editPolicyModalId"
is-edit-mode
/>
</div>
</template>
......@@ -3,10 +3,13 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
export const DELETE_ESCALATION_POLICY_ERROR = s__(
const DELETE_ESCALATION_POLICY_ERROR = s__(
'EscalationPolicies|The escalation policy could not be deleted. Please try again.',
);
const UPDATE_ESCALATION_POLICY_ERROR = s__(
'EscalationPolicies|The escalation policy could not be updated. Please try again',
);
const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, variables) => {
const policy = escalationPolicyCreate?.escalationPolicy;
if (!policy) {
......@@ -29,6 +32,32 @@ const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, va
});
};
const updateEscalationPolicyInStore = (store, query, { escalationPolicyUpdate }, variables) => {
const policy = escalationPolicyUpdate?.escalationPolicy;
if (!policy) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
draftData.project.incidentManagementEscalationPolicies.nodes = draftData.project.incidentManagementEscalationPolicies.nodes.map(
(policyToUpdate) => {
return policyToUpdate.id === policy.id ? policy : policyToUpdate;
},
);
});
store.writeQuery({
query,
variables,
data,
});
};
const deleteEscalationPolicFromStore = (store, query, { escalationPolicyDestroy }, variables) => {
const escalationPolicy = escalationPolicyDestroy?.escalationPolicy;
......@@ -66,6 +95,15 @@ export const updateStoreOnEscalationPolicyCreate = (store, query, data, variable
addEscalationPolicyToStore(store, query, data, variables);
}
};
export const updateStoreOnEscalationPolicyUpdate = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, UPDATE_ESCALATION_POLICY_ERROR);
} else {
updateEscalationPolicyInStore(store, query, data, variables);
}
};
export const updateStoreAfterEscalationPolicyDelete = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, DELETE_ESCALATION_POLICY_ERROR);
......
#import "../fragments/escalation_policy.fragment.graphql"
mutation escalationPolicyUpdate($input: EscalationPolicyUpdateInput!) {
escalationPolicyUpdate(input: $input) {
escalationPolicy {
...EscalationPolicy
}
errors
}
}
......@@ -117,5 +117,11 @@ exports[`EscalationPolicy renders policy with rules 1`] = `
escalationpolicy="[object Object]"
modalid="deleteEscalationPolicyModal-37"
/>
<edit-escalation-policy-modal-stub
escalationpolicy="[object Object]"
iseditmode="true"
modalid="editEscalationPolicyModal-37"
/>
</div>
`;
......@@ -18,6 +18,7 @@ describe('AddEscalationPolicyForm', () => {
form: {
name: mockPolicies[1].name,
description: mockPolicies[1].description,
rules: [],
},
validationState: {
name: true,
......@@ -48,8 +49,14 @@ describe('AddEscalationPolicyForm', () => {
const findAddRuleLink = () => wrapper.findComponent(GlLink);
describe('Escalation rules', () => {
it('should render one default rule', () => {
expect(findRules().length).toBe(1);
it('should render one default rule when rules were not provided', () => {
expect(findRules()).toHaveLength(1);
});
it('should render all the rules if they were provided', async () => {
createComponent({ props: { form: { rules: mockPolicies[1].rules } } });
await wrapper.vm.$nextTick();
expect(findRules()).toHaveLength(mockPolicies[1].rules.length);
});
it('should contain a link to add escalation rules', () => {
......
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import EditEscalationPolicyModal from 'ee/escalation_policies/components/add_edit_escalation_policy_modal.vue';
import DeleteEscalationPolicyModal from 'ee/escalation_policies/components/delete_escalation_policy_modal.vue';
import EscalationPolicy from 'ee/escalation_policies/components/escalation_policy.vue';
import {
deleteEscalationPolicyModalId,
editEscalationPolicyModalId,
} from 'ee/escalation_policies/constants';
import mockPolicies from './mocks/mockPolicies.json';
describe('EscalationPolicy', () => {
let wrapper;
const escalationPolicy = cloneDeep(mockPolicies[0]);
const createComponent = () => {
wrapper = shallowMount(EscalationPolicy, {
propsData: {
policy: cloneDeep(mockPolicies[0]),
policy: escalationPolicy,
index: 0,
},
stubs: {
......@@ -27,7 +35,33 @@ describe('EscalationPolicy', () => {
wrapper.destroy();
});
const findDeleteModal = () => wrapper.findComponent(DeleteEscalationPolicyModal);
const findEditModal = () => wrapper.findComponent(EditEscalationPolicyModal);
it('renders policy with rules', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('Modals', () => {
describe('delete policy modal', () => {
it('should render a modal and provide it with correct id', () => {
const modal = findDeleteModal();
expect(modal.exists()).toBe(true);
expect(modal.props('modalId')).toBe(
`${deleteEscalationPolicyModalId}-${escalationPolicy.id}`,
);
});
});
describe('edit policy modal', () => {
it('should render a modal and provide it with correct id and isEditMode props', () => {
const modal = findEditModal();
expect(modal.exists()).toBe(true);
expect(modal.props('modalId')).toBe(
`${editEscalationPolicyModalId}-${escalationPolicy.id}`,
);
expect(modal.props('isEditMode')).toBe(true);
});
});
});
});
......@@ -44,8 +44,6 @@ describe('Escalation Policies Wrapper', () => {
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findEscalationPolicies = () => wrapper.findAllComponents(EscalationPolicy);
const findAddPolicyBtn = () =>
wrapper.findByRole('button', { name: EscalationPoliciesWrapper.i18n.addPolicy });
describe.each`
state | loading | escalationPolicies | showsEmptyState | showsLoader
......@@ -72,10 +70,6 @@ describe('Escalation Policies Wrapper', () => {
it(`does ${escalationPolicies.length ? 'show' : 'not show'} escalation policies`, () => {
expect(findEscalationPolicies()).toHaveLength(escalationPolicies.length);
});
it(`does ${escalationPolicies.length ? 'show' : 'not show'} "Add policy" button`, () => {
expect(findAddPolicyBtn().exists()).toBe(Boolean(escalationPolicies.length));
});
});
});
});
......@@ -13129,6 +13129,9 @@ msgstr ""
msgid "EscalationPolicies|The escalation policy could not be deleted. Please try again."
msgstr ""
msgid "EscalationPolicies|The escalation policy could not be updated. Please try again"
msgstr ""
msgid "EscalationPolicies|mins"
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