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

Merge branch '262852-adjsut-schedule-timeframe-cache-updates' into 'master'

Fix(oncallschedule): update oncall schedule's apollo cache methods

See merge request gitlab-org/gitlab!52773
parents b7d21377 b3b25252
......@@ -4,7 +4,7 @@ import { isEmpty } from 'lodash';
import { s__, __ } from '~/locale';
import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql';
import updateOncallScheduleMutation from '../graphql/mutations/update_oncall_schedule.mutation.graphql';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import getOncallSchedulesWithRotationsQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import { updateStoreOnScheduleCreate, updateStoreAfterScheduleEdit } from '../utils/cache_updates';
import { isNameFieldValid } from '../utils/common_utils';
import AddEditScheduleForm from './add_edit_schedule_form.vue';
......@@ -106,8 +106,8 @@ export default {
timezone: this.form.timezone.identifier,
},
},
update(store, { data: { oncallScheduleCreate } }) {
updateStoreOnScheduleCreate(store, getOncallSchedulesQuery, oncallScheduleCreate, {
update(store, { data }) {
updateStoreOnScheduleCreate(store, getOncallSchedulesWithRotationsQuery, data, {
projectPath,
});
},
......@@ -143,7 +143,9 @@ export default {
mutation: updateOncallScheduleMutation,
variables: this.editScheduleVariables,
update(store, { data }) {
updateStoreAfterScheduleEdit(store, getOncallSchedulesQuery, data, { projectPath });
updateStoreAfterScheduleEdit(store, getOncallSchedulesWithRotationsQuery, data, {
projectPath,
});
},
})
.then(
......
......@@ -4,8 +4,6 @@ import {
GlCard,
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownItem,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
......@@ -50,8 +48,6 @@ export default {
GlButton,
GlButtonGroup,
GlCard,
GlDropdown,
GlDropdownItem,
GlSprintf,
AddEditRotationModal,
DeleteScheduleModal,
......@@ -86,7 +82,7 @@ export default {
},
update(data) {
const nodes = data.project?.incidentManagementOncallSchedules?.nodes ?? [];
const schedule = nodes.pop() || {};
const schedule = nodes.length ? nodes[nodes.length - 1] : null;
return schedule?.rotations.nodes ?? [];
},
error(error) {
......@@ -202,16 +198,17 @@ export default {
<template #timezone>{{ schedule.timezone }}</template>
</gl-sprintf>
| {{ offset }}
<gl-dropdown right :text="formatPresetType(presetType)">
<gl-dropdown-item
<gl-button-group data-testid="shift-preset-change">
<gl-button
v-for="type in $options.PRESET_TYPES"
:key="type"
:is-check-item="true"
:is-checked="type === presetType"
:selected="type === presetType"
:title="formatPresetType(type)"
@click="switchPresetType(type)"
>{{ formatPresetType(type) }}</gl-dropdown-item
>
</gl-dropdown>
{{ formatPresetType(type) }}
</gl-button>
</gl-button-group>
</p>
<div class="gl-w-full gl-display-flex gl-align-items-center gl-pb-3">
<gl-button-group>
......
<script>
import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/wrapper';
import getOncallSchedulesWithRotations from '../graphql/queries/get_oncall_schedules.query.graphql';
import getOncallSchedulesWithRotationsQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import AddScheduleModal from './add_edit_schedule_modal.vue';
import OncallSchedule from './oncall_schedule.vue';
......@@ -47,8 +46,7 @@ export default {
},
apollo: {
schedule: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getOncallSchedulesWithRotations,
query: getOncallSchedulesWithRotationsQuery,
variables() {
return {
projectPath: this.projectPath,
......
......@@ -4,7 +4,7 @@ import { set } from 'lodash';
import { LENGTH_ENUM } from 'ee/oncall_schedules/constants';
import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql';
import updateOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import {
updateStoreAfterRotationAdd,
updateStoreAfterRotationEdit,
......@@ -152,11 +152,16 @@ export default {
this.$apollo
.mutate({
mutation: createOncallScheduleRotationMutation,
variables: { OncallRotationCreateInput: this.rotationVariables },
variables: { input: this.rotationVariables },
update(store, { data }) {
updateStoreAfterRotationAdd(store, getOncallSchedulesQuery, data, schedule.iid, {
updateStoreAfterRotationAdd(
store,
getOncallSchedulesWithRotationsQuery,
{ ...data, scheduleIid: schedule.iid },
{
projectPath,
});
},
);
},
})
.then(
......@@ -192,11 +197,16 @@ export default {
this.$apollo
.mutate({
mutation: updateOncallScheduleRotationMutation,
variables: { OncallRotationUpdateInput: this.rotationVariables },
variables: { input: this.rotationVariables },
update(store, { data }) {
updateStoreAfterRotationEdit(store, getOncallSchedulesQuery, data, schedule.iid, {
updateStoreAfterRotationEdit(
store,
getOncallSchedulesWithRotationsQuery,
{ ...data, scheduleIid: schedule.iid },
{
projectPath,
});
},
);
},
})
.then(
......
......@@ -103,6 +103,7 @@ export default {
:title="$options.i18n.editRotationLabel"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
:disabled="true"
/>
<gl-button
v-gl-modal="$options.deleteRotationModalId"
......
#import "../fragments/oncall_schedule_rotation.fragment.graphql"
mutation newRotation($OncallRotationCreateInput: OncallRotationCreateInput!) {
oncallRotationCreate(input: $OncallRotationCreateInput) {
mutation newRotation($input: OncallRotationCreateInput!) {
oncallRotationCreate(input: $input) {
errors
oncallRotation {
...OnCallRotation
......
#import "../fragments/oncall_schedule_rotation.fragment.graphql"
mutation updateRotation($OncallRotationUpdateInput: OncallRotationUpdateInput!) {
oncallRotationUpdate(input: $OncallRotationUpdateInput) {
mutation updateRotation($input: OncallRotationUpdateInput!) {
oncallRotationUpdate(input: $input) {
errors
oncallRotation {
...OnCallRotation
......
......@@ -8,7 +8,10 @@ import {
DELETE_ROTATION_ERROR,
} from './error_messages';
const addScheduleToStore = (store, query, { oncallSchedule: schedule }, variables) => {
const ROTATION_CONNECTION_TYPE = 'IncidentManagementOncallRotationConnection';
const addScheduleToStore = (store, query, { oncallScheduleCreate }, variables) => {
const schedule = oncallScheduleCreate?.oncallSchedule;
if (!schedule) {
return;
}
......@@ -19,7 +22,10 @@ const addScheduleToStore = (store, query, { oncallSchedule: schedule }, variable
});
const data = produce(sourceData, (draftData) => {
draftData.project.incidentManagementOncallSchedules.nodes.push(schedule);
draftData.project.incidentManagementOncallSchedules.nodes.push({
...schedule,
rotations: { nodes: [], __typename: ROTATION_CONNECTION_TYPE },
});
});
store.writeQuery({
......@@ -67,10 +73,11 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl
const data = produce(sourceData, (draftData) => {
// eslint-disable-next-line no-param-reassign
draftData.project.incidentManagementOncallSchedules.nodes = [
...draftData.project.incidentManagementOncallSchedules.nodes,
schedule,
];
draftData.project.incidentManagementOncallSchedules.nodes = draftData.project.incidentManagementOncallSchedules.nodes.map(
(scheduleToUpdate) => {
return scheduleToUpdate.iid === schedule.iid ? schedule : scheduleToUpdate;
},
);
});
store.writeQuery({
......@@ -80,13 +87,8 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl
});
};
const addRotationToStore = (
store,
query,
{ oncallRotationCreate: rotation },
scheduleId,
variables,
) => {
const addRotationToStore = (store, query, { oncallRotationCreate, scheduleIid }, variables) => {
const rotation = oncallRotationCreate?.oncallRotation;
if (!rotation) {
return;
}
......@@ -96,14 +98,12 @@ const addRotationToStore = (
variables,
});
// TODO: This needs the rotation backend to be fully integrated to work, for the moment we will place-hold it.
const data = produce(sourceData, (draftData) => {
const rotations = [rotation];
const scheduleToUpdate = draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleIid,
);
// eslint-disable-next-line no-param-reassign
draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleId,
).rotations = rotations;
scheduleToUpdate.rotations.nodes = [...scheduleToUpdate.rotations.nodes, rotation];
});
store.writeQuery({
......@@ -113,7 +113,12 @@ const addRotationToStore = (
});
};
const updateRotationFromStore = (store, query, { oncallRotationUpdate }, scheduleId, variables) => {
const updateRotationFromStore = (
store,
query,
{ oncallRotationUpdate, scheduleIid },
variables,
) => {
const rotation = oncallRotationUpdate?.oncallRotation;
if (!rotation) {
return;
......@@ -125,11 +130,15 @@ const updateRotationFromStore = (store, query, { oncallRotationUpdate }, schedul
});
const data = produce(sourceData, (draftData) => {
// eslint-disable-next-line no-param-reassign
draftData.project.incidentManagementOncallSchedules.nodes = [
...draftData.project.incidentManagementOncallSchedules.nodes,
rotation,
];
const scheduleToUpdate = draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleIid,
);
const updatedRotations = scheduleToUpdate.rotations.nodes.map((rotationToUpdate) => {
return rotationToUpdate.id === rotation.id ? rotation : rotationToUpdate;
});
scheduleToUpdate.rotations.nodes = updatedRotations;
});
store.writeQuery({
......@@ -159,12 +168,12 @@ const deleteRotationFromStore = (
const scheduleToUpdate = draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleIid,
);
const updatedRotations = scheduleToUpdate.rotations?.filter(({ id }) => id !== rotation.id);
// eslint-disable-next-line no-param-reassign
draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleIid,
).rotations = updatedRotations;
const updatedRotations = scheduleToUpdate.rotations.nodes.filter(
({ id }) => id !== rotation.id,
);
scheduleToUpdate.rotations.nodes = updatedRotations;
});
store.writeQuery({
......@@ -203,17 +212,17 @@ export const updateStoreAfterScheduleEdit = (store, query, data, variables) => {
}
};
export const updateStoreAfterRotationAdd = (store, query, data, scheduleId, variables) => {
export const updateStoreAfterRotationAdd = (store, query, data, variables) => {
if (!hasErrors(data)) {
addRotationToStore(store, query, data, scheduleId, variables);
addRotationToStore(store, query, data, variables);
}
};
export const updateStoreAfterRotationEdit = (store, query, data, scheduleId, variables) => {
export const updateStoreAfterRotationEdit = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, UPDATE_ROTATION_ERROR);
} else {
updateRotationFromStore(store, query, data, scheduleId, variables);
updateRotationFromStore(store, query, data, variables);
}
};
......
......@@ -7,7 +7,7 @@ import AddEditScheduleModal, {
import { editScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedule';
import { addScheduleModalId } from 'ee/oncall_schedules/components/oncall_schedules_wrapper';
import updateOncallScheduleMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule.mutation.graphql';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
......@@ -71,14 +71,17 @@ describe('AddScheduleModal', () => {
updateScheduleHandler = updateHandler;
const requestHandlers = [
[getOncallSchedulesQuery, jest.fn().mockResolvedValue(getOncallSchedulesQueryResponse)],
[
getOncallSchedulesWithRotationsQuery,
jest.fn().mockResolvedValue(getOncallSchedulesQueryResponse),
],
[updateOncallScheduleMutation, updateScheduleHandler],
];
fakeApollo = createMockApollo(requestHandlers);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: getOncallSchedulesQuery,
query: getOncallSchedulesWithRotationsQuery,
variables: {
projectPath: 'group/project',
},
......
......@@ -35,7 +35,7 @@ export const getOncallSchedulesQueryResponse = {
timezone: {
identifier: 'Pacific/Honolulu',
},
rotations: { nodes: mockRotations },
rotations: { nodes: [mockRotations] },
},
],
},
......@@ -89,9 +89,7 @@ export const updateScheduleResponse = {
name: 'Test schedule 2',
description: 'Description 2 lives here',
timezone: 'Pacific/Honolulu',
rotations: {
nodes: [],
},
rotations: { nodes: [mockRotations] },
},
},
},
......@@ -107,9 +105,7 @@ export const updateScheduleResponseWithErrors = {
name: 'Test schedule 2',
description: 'Description 2 lives here',
timezone: 'Pacific/Honolulu',
rotations: {
nodes: [],
},
rotations: { nodes: [mockRotations] },
},
},
},
......@@ -140,7 +136,7 @@ export const createRotationResponse = {
oncallRotationCreate: {
errors: [],
oncallRotation: {
id: '37',
id: '44',
name: 'Test',
startsAt: '2020-12-17T12:00:00Z',
length: 5,
......@@ -173,7 +169,7 @@ export const createRotationResponseWithErrors = {
oncallRotationCreate: {
errors: ['Houston, we have a problem'],
oncallRotation: {
id: '37',
id: '44',
name: 'Test',
startsAt: '2020-12-17T12:00:00Z',
length: 5,
......
......@@ -71,12 +71,14 @@ describe('On-call schedule', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findScheduleHeader = () => wrapper.findByTestId('scheduleHeader');
const findRotationsHeader = () => wrapper.findByTestId('rotationsHeader');
const findSchedule = () => wrapper.findByTestId('scheduleBody');
const findRotations = () => wrapper.findByTestId('rotationsBody');
const findRotationsShiftPreset = () => wrapper.findByTestId('shift-preset-change');
const findAddRotationsBtn = () => findRotationsHeader().find(GlButton);
const findScheduleTimeline = () => findRotations().find(ScheduleTimelineSection);
const findRotationsList = () => findRotations().find(RotationsListSection);
......@@ -120,6 +122,25 @@ describe('On-call schedule', () => {
});
});
describe('Timeframe shift preset type', () => {
it('renders rotation shift preset type buttons', () => {
expect(findRotationsShiftPreset().exists()).toBe(true);
});
it('sets shift preset type with a default type', () => {
const presetBtns = findRotationsShiftPreset().findAllComponents(GlButton);
expect(presetBtns.at(0).attributes('selected')).toBe(undefined);
expect(presetBtns.at(1).attributes('selected')).toBe('true');
});
it('updates the rotation preset type on click', async () => {
const presetBtns = findRotationsShiftPreset().findAllComponents(GlButton);
await presetBtns.at(0).vm.$emit('click');
expect(presetBtns.at(0).attributes('selected')).toBe('true');
expect(presetBtns.at(1).attributes('selected')).toBe(undefined);
});
});
describe('Timeframe update', () => {
describe('WEEKS view', () => {
beforeEach(() => {
......
......@@ -6,7 +6,7 @@ import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue';
import OnCallScheduleWrapper, {
i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue';
import getOncallSchedulesWithRotations from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { preExistingSchedule, newlyCreatedSchedule } from './mocks/apollo_mock';
......@@ -44,7 +44,7 @@ describe('On-call schedule wrapper', () => {
function mountComponentWithApollo() {
const fakeApollo = createMockApollo([
[getOncallSchedulesWithRotations, getOncallSchedulesQuerySpy],
[getOncallSchedulesWithRotationsQuery, getOncallSchedulesQuerySpy],
]);
localVue.use(VueApollo);
......@@ -66,6 +66,7 @@ describe('On-call schedule wrapper', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
......
......@@ -6,7 +6,7 @@ import AddEditRotationModal, {
} from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue';
import { addRotationModalId } from 'ee/oncall_schedules/constants';
import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash, { FLASH_TYPES } from '~/flash';
......@@ -80,13 +80,16 @@ describe('AddEditRotationModal', () => {
localVue.use(VueApollo);
fakeApollo = createMockApollo([
[getOncallSchedulesQuery, jest.fn().mockResolvedValue(getOncallSchedulesQueryResponse)],
[
getOncallSchedulesWithRotationsQuery,
jest.fn().mockResolvedValue(getOncallSchedulesQueryResponse),
],
[usersSearchQuery, userSearchQueryHandler],
[createOncallScheduleRotationMutation, createRotationHandler],
]);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: getOncallSchedulesQuery,
query: getOncallSchedulesWithRotationsQuery,
variables: {
projectPath: 'group/project',
},
......@@ -123,6 +126,7 @@ describe('AddEditRotationModal', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findModal = () => wrapper.find(GlModal);
......@@ -139,7 +143,7 @@ describe('AddEditRotationModal', () => {
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
update: expect.anything(),
variables: { OncallRotationCreateInput: expect.objectContaining({ projectPath }) },
variables: { input: expect.objectContaining({ projectPath }) },
});
});
......@@ -155,7 +159,7 @@ describe('AddEditRotationModal', () => {
});
describe('with mocked Apollo client', () => {
it('it calls searchUsers query with the search paramter', async () => {
it('it calls searchUsers query with the search parameter', async () => {
userSearchQueryHandler = jest.fn().mockResolvedValue({
data: {
users: {
......@@ -168,9 +172,7 @@ describe('AddEditRotationModal', () => {
expect(userSearchQueryHandler).toHaveBeenCalledWith({ search: 'root' });
});
// Fix is coming in: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52773/
// eslint-disable-next-line jest/no-disabled-tests
it.skip('calls a mutation with correct parameters and creates a rotation', async () => {
it('calls a mutation with correct parameters and creates a rotation', async () => {
createComponentWithApollo();
await createRotation(wrapper);
......@@ -184,9 +186,7 @@ describe('AddEditRotationModal', () => {
});
});
// Fix is coming in: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52773/
// eslint-disable-next-line jest/no-disabled-tests
it.skip('displays alert if mutation had a recoverable error', async () => {
it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
createHandler: jest.fn().mockResolvedValue(createRotationResponseWithErrors),
});
......
......@@ -109,6 +109,7 @@ describe('DeleteRotationModal', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders delete rotation modal layout', () => {
......@@ -161,9 +162,7 @@ describe('DeleteRotationModal', () => {
expect(findModal().attributes('data-testid')).toBe(`delete-rotation-modal-${rotation.id}`);
});
// Fix is coming in: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52773/
// eslint-disable-next-line jest/no-disabled-tests
it.skip('calls a mutation with correct parameters and destroys a rotation', async () => {
it('calls a mutation with correct parameters and destroys a rotation', async () => {
createComponentWithApollo();
await destroyRotation(wrapper);
......@@ -171,9 +170,7 @@ describe('DeleteRotationModal', () => {
expect(destroyRotationHandler).toHaveBeenCalled();
});
// Fix is coming in: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52773/
// eslint-disable-next-line jest/no-disabled-tests
it.skip('displays alert if mutation had a recoverable error', async () => {
it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockResolvedValue(destroyRotationResponseWithErrors),
});
......
......@@ -22,7 +22,8 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders
>
<button
aria-label="Edit rotation"
class="btn btn-default btn-md gl-button btn-default-tertiary btn-icon"
class="btn btn-default btn-md disabled gl-button btn-default-tertiary btn-icon"
disabled="disabled"
title="Edit rotation"
type="button"
>
......
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