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