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 @@
import { isEmpty } from 'lodash';
import { GlModal, GlAlert } from '@gitlab/ui';
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 { 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 { updateStoreOnScheduleCreate, updateStoreAfterScheduleEdit } from '../utils/cache_updates';
export const i18n = {
cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add 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 {
......@@ -22,31 +25,37 @@ export default {
AddEditScheduleForm,
},
props: {
schedule: {
type: Object,
required: true,
},
modalId: {
type: String,
required: true,
},
schedule: {
type: Object,
required: false,
default: () => ({}),
},
isEditMode: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
loading: false,
error: null,
form: {
name: this.schedule.name,
description: this.schedule.description,
timezone: this.timezones.find(({ identifier }) => this.schedule.timezone === identifier),
name: this.schedule?.name,
description: this.schedule?.description,
timezone: this.timezones.find(({ identifier }) => this.schedule?.timezone === identifier),
},
error: null,
};
},
computed: {
actionsProps() {
return {
primary: {
text: i18n.editSchedule,
text: this.title,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
......@@ -59,7 +68,7 @@ export default {
};
},
isNameInvalid() {
return !this.form.name.length;
return !this.form.name?.length;
},
isTimezoneInvalid() {
return isEmpty(this.form.timezone);
......@@ -76,8 +85,53 @@ export default {
timezone: this.form.timezone.identifier,
};
},
errorMsg() {
return this.error || (this.isEditMode ? i18n.editErrorMsg : i18n.addErrorMsg);
},
title() {
return this.isEditMode ? i18n.editSchedule : i18n.addSchedule;
},
},
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() {
const { projectPath } = this;
this.loading = true;
......@@ -94,7 +148,7 @@ export default {
if (error) {
throw error;
}
this.$refs.updateScheduleModal.hide();
this.$refs.addUpdateScheduleModal.hide();
})
.catch(error => {
this.error = error;
......@@ -115,17 +169,16 @@ export default {
<template>
<gl-modal
ref="updateScheduleModal"
ref="addUpdateScheduleModal"
:modal-id="modalId"
size="sm"
:data-testid="`update-schedule-modal-${schedule.iid}`"
:title="$options.i18n.editSchedule"
:title="title"
:action-primary="actionsProps.primary"
: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">
{{ error || $options.i18n.errorMsg }}
{{ errorMsg }}
</gl-alert>
<add-edit-schedule-form
: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';
import { s__, __ } from '~/locale';
import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.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 { getTimeframeForWeeksView } from './schedule/utils';
......@@ -146,6 +146,11 @@ export default {
</gl-card>
<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"
is-edit-mode
/>
<add-rotation-modal :schedule="schedule" :modal-id="$options.addRotationModalId" />
</div>
</template>
......@@ -2,7 +2,7 @@
import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import mockRotations from '../../../../../spec/frontend/oncall_schedule/mocks/mock_rotation.json';
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 { s__ } from '~/locale';
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, {
i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.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 getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
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