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, createLocalVue } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlModal, GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import AddEditScheduleModal, {
i18n,
} from 'ee/oncall_schedules/components/add_edit_schedule_modal.vue';
import { addScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedules_wrapper';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import updateOncallScheduleMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule.mutation.graphql';
import UpdateScheduleModal, { i18n } from 'ee/oncall_schedules/components/edit_schedule_modal.vue';
import { editScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedule';
import {
getOncallSchedulesQueryResponse,
......@@ -14,48 +17,28 @@ import {
} from './mocks/apollo_mock';
import mockTimezones from './mocks/mockTimezones.json';
const localVue = createLocalVue();
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
const schedule =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
localVue.use(VueApollo);
describe('UpdateScheduleModal', () => {
describe('AddScheduleModal', () => {
let wrapper;
let fakeApollo;
const localVue = createLocalVue();
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
const mockSchedule =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
let updateScheduleHandler;
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
async function awaitApolloDomMock() {
await wrapper.vm.$nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
}
async function updateSchedule(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
localWrapper.find(GlModal).vm.$emit('primary', { preventDefault: jest.fn() });
}
const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(UpdateScheduleModal, {
const createComponent = ({ schedule, isEditMode, modalId } = {}) => {
wrapper = shallowMount(AddEditScheduleModal, {
data() {
return {
...data,
form: schedule,
form: mockSchedule,
};
},
propsData: {
modalId: editScheduleModalId,
modalId,
schedule,
...props,
isEditMode,
},
provide: {
projectPath,
......@@ -67,12 +50,26 @@ describe('UpdateScheduleModal', () => {
},
},
});
wrapper.vm.$refs.updateScheduleModal.hide = mockHideModal;
wrapper.vm.$refs.addUpdateScheduleModal.hide = mockHideModal;
};
function createComponentWithApollo({
async function awaitApolloDomMock() {
await wrapper.vm.$nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
}
async function updateSchedule(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
localWrapper.find(GlModal).vm.$emit('primary', { preventDefault: jest.fn() });
}
const createComponentWithApollo = ({
updateHandler = jest.fn().mockResolvedValue(updateScheduleResponse),
} = {}) {
} = {}) => {
localVue.use(VueApollo);
updateScheduleHandler = updateHandler;
......@@ -91,36 +88,87 @@ describe('UpdateScheduleModal', () => {
data: getOncallSchedulesQueryResponse.data,
});
wrapper = shallowMount(UpdateScheduleModal, {
wrapper = shallowMount(AddEditScheduleModal, {
localVue,
apolloProvider: fakeApollo,
data() {
return {
form: schedule,
form: mockSchedule,
};
},
propsData: {
modalId: editScheduleModalId,
schedule,
isEditMode: true,
schedule: mockSchedule,
},
provide: {
projectPath,
timezones: mockTimezones,
},
});
}
beforeEach(() => {
createComponent();
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders update schedule modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
describe('Schedule create', () => {
beforeEach(() => {
createComponent({ modalId: addScheduleModalId });
});
describe('renders create modal with the correct schedule information', () => {
it('renders name of correct modal id', () => {
expect(findModal().attributes('modalid')).toBe(addScheduleModalId);
});
it('renders modal title', () => {
expect(findModal().attributes('title')).toBe(i18n.addSchedule);
});
});
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,
...mockSchedule,
timezone: mockSchedule.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);
});
});
describe('Schedule update', () => {
beforeEach(() => {
createComponent({ schedule: mockSchedule, isEditMode: true, modalId: editScheduleModalId });
});
describe('renders update modal with the correct schedule information', () => {
......@@ -128,8 +176,8 @@ describe('UpdateScheduleModal', () => {
expect(findModal().attributes('modalid')).toBe(editScheduleModalId);
});
it('renders name of schedule to update', () => {
expect(findModal().html()).toContain(i18n.editSchedule);
it('renders modal title', () => {
expect(findModal().attributes('title')).toBe(i18n.editSchedule);
});
});
......@@ -141,11 +189,11 @@ describe('UpdateScheduleModal', () => {
mutation: expect.any(Object),
update: expect.anything(),
variables: {
iid: schedule.iid,
iid: mockSchedule.iid,
projectPath,
name: schedule.name,
description: schedule.description,
timezone: schedule.timezone.identifier,
name: mockSchedule.name,
description: mockSchedule.description,
timezone: mockSchedule.timezone.identifier,
},
});
});
......@@ -167,15 +215,6 @@ describe('UpdateScheduleModal', () => {
});
describe('with mocked Apollo client', () => {
it('has the name of the schedule to update based on getOncallSchedulesQuery', async () => {
createComponentWithApollo();
await jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(findModal().attributes('data-testid')).toBe(`update-schedule-modal-${schedule.iid}`);
});
it('calls a mutation with correct parameters and updates a schedule', async () => {
createComponentWithApollo();
......@@ -197,4 +236,5 @@ describe('UpdateScheduleModal', () => {
expect(alert.text()).toContain('Houston, we have a problem');
});
});
});
});
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