Commit 6cf182a3 authored by Phil Hughes's avatar Phil Hughes

Merge branch '292985-join-create-and-edit-modals' into 'master'

Join add and update schedule modals

See merge request gitlab-org/gitlab!49973
parents 9bbffb78 5fb1ec11
...@@ -2,15 +2,18 @@ ...@@ -2,15 +2,18 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import updateOncallScheduleMutation from '../graphql/mutations/update_oncall_schedule.mutation.graphql';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql'; import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import { updateStoreAfterScheduleEdit } from '../utils/cache_updates'; import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql';
import updateOncallScheduleMutation from '../graphql/mutations/update_oncall_schedule.mutation.graphql';
import AddEditScheduleForm from './add_edit_schedule_form.vue'; import AddEditScheduleForm from './add_edit_schedule_form.vue';
import { updateStoreOnScheduleCreate, updateStoreAfterScheduleEdit } from '../utils/cache_updates';
export const i18n = { export const i18n = {
cancel: __('Cancel'), cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add schedule'),
editSchedule: s__('OnCallSchedules|Edit schedule'), editSchedule: s__('OnCallSchedules|Edit schedule'),
errorMsg: s__('OnCallSchedules|Failed to edit schedule'), addErrorMsg: s__('OnCallSchedules|Failed to edit schedule'),
editErrorMsg: s__('OnCallSchedules|Failed to add schedule'),
}; };
export default { export default {
...@@ -22,31 +25,37 @@ export default { ...@@ -22,31 +25,37 @@ export default {
AddEditScheduleForm, AddEditScheduleForm,
}, },
props: { props: {
schedule: {
type: Object,
required: true,
},
modalId: { modalId: {
type: String, type: String,
required: true, required: true,
}, },
schedule: {
type: Object,
required: false,
default: () => ({}),
},
isEditMode: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
loading: false, loading: false,
error: null,
form: { form: {
name: this.schedule.name, name: this.schedule?.name,
description: this.schedule.description, description: this.schedule?.description,
timezone: this.timezones.find(({ identifier }) => this.schedule.timezone === identifier), timezone: this.timezones.find(({ identifier }) => this.schedule?.timezone === identifier),
}, },
error: null,
}; };
}, },
computed: { computed: {
actionsProps() { actionsProps() {
return { return {
primary: { primary: {
text: i18n.editSchedule, text: this.title,
attributes: [ attributes: [
{ variant: 'info' }, { variant: 'info' },
{ loading: this.loading }, { loading: this.loading },
...@@ -59,7 +68,7 @@ export default { ...@@ -59,7 +68,7 @@ export default {
}; };
}, },
isNameInvalid() { isNameInvalid() {
return !this.form.name.length; return !this.form.name?.length;
}, },
isTimezoneInvalid() { isTimezoneInvalid() {
return isEmpty(this.form.timezone); return isEmpty(this.form.timezone);
...@@ -76,8 +85,53 @@ export default { ...@@ -76,8 +85,53 @@ export default {
timezone: this.form.timezone.identifier, timezone: this.form.timezone.identifier,
}; };
}, },
errorMsg() {
return this.error || (this.isEditMode ? i18n.editErrorMsg : i18n.addErrorMsg);
},
title() {
return this.isEditMode ? i18n.editSchedule : i18n.addSchedule;
},
}, },
methods: { methods: {
createSchedule() {
this.loading = true;
const { projectPath } = this;
this.$apollo
.mutate({
mutation: createOncallScheduleMutation,
variables: {
oncallScheduleCreateInput: {
projectPath,
...this.form,
timezone: this.form.timezone.identifier,
},
},
update(
store,
{
data: { oncallScheduleCreate },
},
) {
updateStoreOnScheduleCreate(store, getOncallSchedulesQuery, oncallScheduleCreate, {
projectPath,
});
},
})
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.addUpdateScheduleModal.hide();
this.$emit('scheduleCreated');
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
editSchedule() { editSchedule() {
const { projectPath } = this; const { projectPath } = this;
this.loading = true; this.loading = true;
...@@ -94,7 +148,7 @@ export default { ...@@ -94,7 +148,7 @@ export default {
if (error) { if (error) {
throw error; throw error;
} }
this.$refs.updateScheduleModal.hide(); this.$refs.addUpdateScheduleModal.hide();
}) })
.catch(error => { .catch(error => {
this.error = error; this.error = error;
...@@ -115,17 +169,16 @@ export default { ...@@ -115,17 +169,16 @@ export default {
<template> <template>
<gl-modal <gl-modal
ref="updateScheduleModal" ref="addUpdateScheduleModal"
:modal-id="modalId" :modal-id="modalId"
size="sm" size="sm"
:data-testid="`update-schedule-modal-${schedule.iid}`" :title="title"
:title="$options.i18n.editSchedule"
:action-primary="actionsProps.primary" :action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel" :action-cancel="actionsProps.cancel"
@primary.prevent="editSchedule" @primary.prevent="isEditMode ? editSchedule() : createSchedule()"
> >
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert"> <gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }} {{ errorMsg }}
</gl-alert> </gl-alert>
<add-edit-schedule-form <add-edit-schedule-form
:is-name-invalid="isNameInvalid" :is-name-invalid="isNameInvalid"
......
<script>
import { isEmpty } from 'lodash';
import { GlModal, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql';
import AddEditScheduleForm from './add_edit_schedule_form.vue';
import { updateStoreOnScheduleCreate } from '../utils/cache_updates';
export const i18n = {
cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add schedule'),
errorMsg: s__('OnCallSchedules|Failed to add schedule'),
};
export default {
i18n,
inject: ['projectPath', 'timezones'],
components: {
GlModal,
GlAlert,
AddEditScheduleForm,
},
props: {
modalId: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
form: {
name: '',
description: '',
timezone: '',
},
error: null,
};
},
computed: {
actionsProps() {
return {
primary: {
text: i18n.addSchedule,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
{ disabled: this.isFormInvalid },
],
},
cancel: {
text: i18n.cancel,
},
};
},
isNameInvalid() {
return !this.form.name.length;
},
isTimezoneInvalid() {
return isEmpty(this.form.timezone);
},
isFormInvalid() {
return this.isNameInvalid || this.isTimezoneInvalid;
},
},
methods: {
createSchedule() {
this.loading = true;
const { projectPath } = this;
this.$apollo
.mutate({
mutation: createOncallScheduleMutation,
variables: {
oncallScheduleCreateInput: {
projectPath,
...this.form,
timezone: this.form.timezone.identifier,
},
},
update(
store,
{
data: { oncallScheduleCreate },
},
) {
updateStoreOnScheduleCreate(store, getOncallSchedulesQuery, oncallScheduleCreate, {
projectPath,
});
},
})
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.createScheduleModal.hide();
this.$emit('scheduleCreated');
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
hideErrorAlert() {
this.error = null;
},
updateScheduleForm({ type, value }) {
this.form[type] = value;
},
},
};
</script>
<template>
<gl-modal
ref="createScheduleModal"
:modal-id="modalId"
size="sm"
:title="$options.i18n.addSchedule"
:action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel"
@primary.prevent="createSchedule"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<add-edit-schedule-form
:is-name-invalid="isNameInvalid"
:is-timezone-invalid="isTimezoneInvalid"
:form="form"
@update-schedule-form="updateScheduleForm"
/>
</gl-modal>
</template>
...@@ -11,7 +11,7 @@ import { formatDate } from '~/lib/utils/datetime_utility'; ...@@ -11,7 +11,7 @@ import { formatDate } from '~/lib/utils/datetime_utility';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue'; import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue';
import DeleteScheduleModal from './delete_schedule_modal.vue'; import DeleteScheduleModal from './delete_schedule_modal.vue';
import EditScheduleModal from './edit_schedule_modal.vue'; import EditScheduleModal from './add_edit_schedule_modal.vue';
import AddRotationModal from './rotations/components/add_rotation_modal.vue'; import AddRotationModal from './rotations/components/add_rotation_modal.vue';
import { getTimeframeForWeeksView } from './schedule/utils'; import { getTimeframeForWeeksView } from './schedule/utils';
...@@ -146,6 +146,11 @@ export default { ...@@ -146,6 +146,11 @@ export default {
</gl-card> </gl-card>
<delete-schedule-modal :schedule="schedule" :modal-id="$options.deleteScheduleModalId" /> <delete-schedule-modal :schedule="schedule" :modal-id="$options.deleteScheduleModalId" />
<edit-schedule-modal :schedule="schedule" :modal-id="$options.editScheduleModalId" /> <edit-schedule-modal :schedule="schedule" :modal-id="$options.editScheduleModalId" />
<edit-schedule-modal
:schedule="schedule"
:modal-id="$options.editScheduleModalId"
is-edit-mode
/>
<add-rotation-modal :schedule="schedule" :modal-id="$options.addRotationModalId" /> <add-rotation-modal :schedule="schedule" :modal-id="$options.addRotationModalId" />
</div> </div>
</template> </template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui'; import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import mockRotations from '../../../../../spec/frontend/oncall_schedule/mocks/mock_rotation.json'; import mockRotations from '../../../../../spec/frontend/oncall_schedule/mocks/mock_rotation.json';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import AddScheduleModal from './add_schedule_modal.vue'; import AddScheduleModal from './add_edit_schedule_modal.vue';
import OncallSchedule from './oncall_schedule.vue'; import OncallSchedule from './oncall_schedule.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql'; import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddScheduleModal renders modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="addScheduleModal"
size="sm"
title="Add schedule"
titletag="h4"
>
<!---->
<add-edit-schedule-form-stub
form="[object Object]"
schedule="[object Object]"
/>
</gl-modal-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UpdateScheduleModal renders update schedule modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
data-testid="update-schedule-modal-37"
modalclass=""
modalid="editScheduleModal"
size="sm"
title="Edit schedule"
titletag="h4"
>
<!---->
<add-edit-schedule-form-stub
form="[object Object]"
schedule="[object Object]"
/>
</gl-modal-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import { addScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedules_wrapper';
import { getOncallSchedulesQueryResponse } from './mocks/apollo_mock';
import mockTimezones from './mocks/mockTimezones.json';
describe('AddScheduleModal', () => {
let wrapper;
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
const formData =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(AddScheduleModal, {
data() {
return {
form: formData,
...data,
};
},
propsData: {
modalId: addScheduleModalId,
...props,
},
provide: {
projectPath,
timezones: mockTimezones,
},
mocks: {
$apollo: {
mutate,
},
},
});
wrapper.vm.$refs.createScheduleModal.hide = mockHideModal;
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
it('renders modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('Schedule create', () => {
it('makes a request with form data to create a schedule', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
update: expect.any(Function),
variables: {
oncallScheduleCreateInput: {
projectPath,
...formData,
timezone: formData.timezone.identifier,
},
},
});
});
it('hides the modal on successful schedule creation', async () => {
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide a modal and shows error alert on fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
});
});
...@@ -4,7 +4,7 @@ import OnCallScheduleWrapper, { ...@@ -4,7 +4,7 @@ import OnCallScheduleWrapper, {
i18n, i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue'; } from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue';
import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue'; import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue';
import AddScheduleModal from 'ee/oncall_schedules/components/add_schedule_modal.vue'; import AddScheduleModal from 'ee/oncall_schedules/components/add_edit_schedule_modal.vue';
import createMockApollo from 'jest/helpers/mock_apollo_helper'; import createMockApollo from 'jest/helpers/mock_apollo_helper';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql'; import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
......
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