Commit ffe73138 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'tr-on-call-rotation-interval-frontend' into 'master'

Update on-call rotation time interval restrictions

See merge request gitlab-org/gitlab!54280
parents 444b1ba2 33576731
......@@ -101,7 +101,6 @@ export default {
return {
participantsArr: [],
endDateEnabled: false,
restrictToTimeEnabled: false,
};
},
methods: {
......@@ -295,15 +294,21 @@ export default {
</gl-card>
<gl-toggle
v-model="restrictToTimeEnabled"
:value="form.isRestrictedToTime"
data-testid="restricted-to-toggle"
:label="$options.i18n.fields.restrictToTime.enableToggle"
label-position="left"
class="gl-mt-5"
@change="
$emit('update-rotation-form', {
type: 'isRestrictedToTime',
value: !form.isRestrictedToTime,
})
"
/>
<gl-card
v-if="restrictToTimeEnabled"
v-if="form.isRestrictedToTime"
data-testid="restricted-to-time"
class="gl-mt-5 gl-border-gray-400 gl-bg-gray-10"
>
......@@ -317,15 +322,17 @@ export default {
<span> {{ __('From') }} </span>
<gl-dropdown
data-testid="restricted-from"
:text="format24HourTimeStringFromInt(form.restrictedTo.from)"
:text="format24HourTimeStringFromInt(form.restrictedTo.startTime)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.from === time"
:is-checked="form.restrictedTo.startTime === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.from', value: time })"
@click="
$emit('update-rotation-form', { type: 'restrictedTo.startTime', value: time })
"
>
<span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span
......@@ -335,15 +342,17 @@ export default {
<span> {{ __('To') }} </span>
<gl-dropdown
data-testid="restricted-to"
:text="format24HourTimeStringFromInt(form.restrictedTo.to)"
:text="format24HourTimeStringFromInt(form.restrictedTo.endTime)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.to === time"
:is-checked="form.restrictedTo.endTime === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.to', value: time })"
@click="
$emit('update-rotation-form', { type: 'restrictedTo.endTime', value: time })
"
>
<span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span
......
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { set } from 'lodash';
import { cloneDeep, 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';
......@@ -21,9 +21,30 @@ export const i18n = {
cancel: __('Cancel'),
};
export const formEmptyState = {
name: '',
participants: [],
rotationLength: {
length: 1,
unit: LENGTH_ENUM.days,
},
startsAt: {
date: null,
time: 0,
},
endsAt: {
date: null,
time: 0,
},
isRestrictedToTime: false,
restrictedTo: {
startTime: 0,
endTime: 0,
},
};
export default {
i18n,
LENGTH_ENUM,
components: {
GlModal,
GlAlert,
......@@ -67,26 +88,7 @@ export default {
participants: [],
loading: false,
ptSearchTerm: '',
form: {
name: '',
participants: [],
rotationLength: {
length: 1,
unit: this.$options.LENGTH_ENUM.days,
},
startsAt: {
date: null,
time: 0,
},
endsAt: {
date: null,
time: 0,
},
restrictedTo: {
from: 0,
to: 0,
},
},
form: cloneDeep(formEmptyState),
error: '',
validationState: {
name: true,
......@@ -134,7 +136,7 @@ export default {
endsAt: { date: endDate, time: endTime },
} = this.form;
return {
const variables = {
projectPath: this.projectPath,
scheduleIid: this.schedule.iid,
name,
......@@ -154,6 +156,13 @@ export default {
},
participants: getParticipantsForSave(participants),
};
if (this.form.isRestrictedToTime) {
variables.activePeriod = {
startTime: format24HourTimeStringFromInt(this.form.restrictedTo.startTime),
endTime: format24HourTimeStringFromInt(this.form.restrictedTo.endTime),
};
}
return variables;
},
title() {
return this.isEditMode ? this.$options.i18n.editRotation : this.$options.i18n.addRotation;
......@@ -273,6 +282,9 @@ export default {
this.validationState.endsAt = this.isEndDateValid;
}
},
afterCloseModal() {
this.form = cloneDeep(formEmptyState);
},
},
};
</script>
......@@ -286,6 +298,7 @@ export default {
:action-cancel="actionsProps.cancel"
modal-class="rotations-modal"
@primary.prevent="isEditMode ? editRotation() : createRotation()"
@hide="afterCloseModal"
>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">
{{ error || $options.i18n.errorMsg }}
......
......@@ -135,6 +135,7 @@ export default {
:title="$options.i18n.editRotationLabel"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
@click="setRotationToUpdate(rotation)"
/>
<gl-button
v-gl-modal="$options.deleteRotationModalId"
......
......@@ -7,6 +7,10 @@ fragment OnCallRotation on IncidentManagementOncallRotation {
endsAt
length
lengthUnit
activePeriod {
startTime
endTime
}
participants {
nodes {
...OnCallParticipant
......
......@@ -10,6 +10,7 @@ export const participants = [
name: 'test',
avatar: '',
avatarUrl: '',
webUrl: '',
},
{
id: '2',
......@@ -17,6 +18,7 @@ export const participants = [
name: 'hello',
avatar: '',
avatarUrl: '',
webUrl: '',
},
];
......@@ -142,6 +144,10 @@ export const createRotationResponse = {
endsAt: '2021-03-17T12:00:00Z',
length: 5,
lengthUnit: 'WEEKS',
activePeriod: {
startTime: '02:00',
endTime: '10:00',
},
participants: {
nodes: [
{
......@@ -176,6 +182,10 @@ export const createRotationResponseWithErrors = {
endsAt: '2021-03-17T12:00:00Z',
length: 5,
lengthUnit: 'WEEKS',
activePeriod: {
startTime: '02:00',
endTime: '10:00',
},
participants: {
nodes: [
{
......
......@@ -5,6 +5,10 @@
"endsAt": "2021-03-13T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"activePeriod": {
"startTime": "02:00",
"endTime": "10:00"
},
"participants": {
"nodes": [
{
......@@ -58,6 +62,10 @@
"endsAt": "2021-03-13T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"activePeriod": {
"startTime": "02:00",
"endTime": "10:00"
},
"participants": {
"nodes": [
{
......@@ -107,6 +115,10 @@
"endsAt": "2021-01-10T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"activePeriod": {
"startTime": "02:00",
"endTime": "10:00"
},
"participants": {
"nodes": [
{
......@@ -156,6 +168,10 @@
"endsAt": "2021-01-11T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"activePeriod": {
"startTime": "02:00",
"endTime": "10:00"
},
"participants": {
"nodes": [
{
......
import { GlDropdownItem, GlTokenSelector, GlFormGroup, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep, merge } from 'lodash';
import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue';
import { formEmptyState } from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue';
import { LENGTH_ENUM } from 'ee/oncall_schedules/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { participants, getOncallSchedulesQueryResponse } from '../../mocks/apollo_mock';
const projectPath = 'group/project';
......@@ -19,47 +20,26 @@ describe('AddEditRotationForm', () => {
...data,
};
},
propsData: {
...props,
schedule,
isLoading: false,
validationState: {
name: true,
participants: false,
startsAt: false,
},
participants,
form: {
name: '',
participants: [],
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
},
startsAt: {
date: null,
time: 0,
},
endsAt: {
date: null,
time: 0,
},
restrictedTo: {
from: 0,
to: 0,
propsData: merge(
{
schedule,
isLoading: false,
validationState: {
name: true,
participants: false,
startsAt: false,
},
participants,
form: cloneDeep(formEmptyState),
},
},
props,
),
provide: {
projectPath,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
......@@ -75,7 +55,6 @@ describe('AddEditRotationForm', () => {
const findRotationFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findStartsOnTimeOptions = () => findRotationStartTime().findAllComponents(GlDropdownItem);
const findEndsOnTimeOptions = () => findRotationEndTime().findAllComponents(GlDropdownItem);
const findRestrictedToTime = () => wrapper.find('[data-testid="restricted-to-time"]');
const findRestrictedToToggle = () => wrapper.find('[data-testid="restricted-to-toggle"]');
const findRestrictedFromOptions = () =>
wrapper.find('[data-testid="restricted-from"]').findAllComponents(GlDropdownItem);
......@@ -83,6 +62,10 @@ describe('AddEditRotationForm', () => {
wrapper.find('[data-testid="restricted-to"]').findAllComponents(GlDropdownItem);
describe('Rotation form validation', () => {
beforeEach(() => {
createComponent();
});
it.each`
index | type | validationState | value
${0} | ${'name'} | ${true} | ${'true'}
......@@ -99,12 +82,14 @@ describe('AddEditRotationForm', () => {
describe('Rotation length and start time', () => {
it('renders the rotation length value', async () => {
createComponent();
const rotationLength = findRotationLength();
expect(rotationLength.exists()).toBe(true);
expect(rotationLength.attributes('value')).toBe('1');
});
it('renders the rotation starts on datepicker', async () => {
createComponent();
const startsOn = findRotationStartTime();
expect(startsOn.exists()).toBe(true);
expect(startsOn.attributes('text')).toBe('00:00');
......@@ -113,8 +98,8 @@ describe('AddEditRotationForm', () => {
it('should emit an event with selected value on time selection', async () => {
const option = 3;
createComponent();
findStartsOnTimeOptions().at(option).vm.$emit('click');
await wrapper.vm.$nextTick();
const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'startsAt.time', value: option });
......@@ -122,14 +107,16 @@ describe('AddEditRotationForm', () => {
it('should add a checkmark to a selected start time', async () => {
const time = 7;
wrapper.setProps({
form: {
startsAt: {
time,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
createComponent({
props: {
form: {
startsAt: {
time,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
},
},
},
});
......@@ -140,9 +127,9 @@ describe('AddEditRotationForm', () => {
describe('Rotation end time', () => {
it('toggles end time visibility', async () => {
createComponent();
const toggle = findEndDateToggle().vm;
toggle.$emit('change', false);
await wrapper.vm.$nextTick();
expect(findRotationEndsContainer().exists()).toBe(false);
toggle.$emit('change', true);
await wrapper.vm.$nextTick();
......@@ -150,97 +137,102 @@ describe('AddEditRotationForm', () => {
});
it('should emit an event with selected value on time selection', async () => {
createComponent();
findEndDateToggle().vm.$emit('change', true);
await wrapper.vm.$nextTick();
const option = 3;
findEndsOnTimeOptions().at(option).vm.$emit('click');
await wrapper.vm.$nextTick();
const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'endsAt.time', value: option });
});
it('should add a checkmark to a selected end time', async () => {
findEndDateToggle().vm.$emit('change', true);
const time = 5;
wrapper.setProps({
form: {
endsAt: {
time,
},
startsAt: {
time: 0,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
createComponent({
props: {
form: {
endsAt: {
time,
},
startsAt: {
time: 0,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
},
},
},
});
findEndDateToggle().vm.$emit('change', true);
await wrapper.vm.$nextTick();
expect(findEndsOnTimeOptions().at(time).props('isChecked')).toBe(true);
});
});
describe('Rotation restricted to time', () => {
it('toggles restricted to time visibility', async () => {
it('toggle state depends on isRestrictedToTime', async () => {
createComponent();
expect(findRestrictedToToggle().props('value')).toBe(false);
createComponent({ props: { form: { ...formEmptyState, isRestrictedToTime: true } } });
expect(findRestrictedToToggle().props('value')).toBe(true);
});
it('toggles end time visibility on', async () => {
createComponent();
const toggle = findRestrictedToToggle().vm;
toggle.$emit('change', false);
await wrapper.vm.$nextTick();
expect(findRestrictedToTime().exists()).toBe(false);
toggle.$emit('change', true);
await wrapper.vm.$nextTick();
expect(findRestrictedToTime().exists()).toBe(true);
const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'isRestrictedToTime', value: true });
});
it('should emit an event with selected value on restricted FROM time selection', async () => {
findRestrictedToToggle().vm.$emit('change', true);
await wrapper.vm.$nextTick();
const timeFrom = 5;
const timeTo = 22;
findRestrictedFromOptions().at(timeFrom).vm.$emit('click');
findRestrictedToOptions().at(timeTo).vm.$emit('click');
await wrapper.vm.$nextTick();
it('toggles end time visibility off', async () => {
createComponent({ props: { form: { ...formEmptyState, isRestrictedToTime: true } } });
const toggle = findRestrictedToToggle().vm;
toggle.$emit('change', false);
const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(2);
expect(emittedEvent[0][0]).toEqual({ type: 'restrictedTo.from', value: timeFrom + 1 });
expect(emittedEvent[1][0]).toEqual({ type: 'restrictedTo.to', value: timeTo + 1 });
expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'isRestrictedToTime', value: false });
});
it('should add a checkmark to a selected restricted FROM time', async () => {
findRestrictedToToggle().vm.$emit('change', true);
describe('when a rotation restriction is selected', () => {
const timeFrom = 5;
const timeTo = 22;
wrapper.setProps({
form: {
endsAt: {
time: 0,
},
startsAt: {
time: 0,
},
restrictedTo: {
from: timeFrom,
to: timeTo,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
it('should emit an event with selected value on restricted FROM time selection', async () => {
createComponent({ props: { form: { ...formEmptyState, isRestrictedToTime: true } } });
findRestrictedFromOptions().at(timeFrom).vm.$emit('click');
findRestrictedToOptions().at(timeTo).vm.$emit('click');
const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(2);
expect(emittedEvent[0][0]).toEqual({ type: 'restrictedTo.startTime', value: timeFrom + 1 });
expect(emittedEvent[1][0]).toEqual({ type: 'restrictedTo.endTime', value: timeTo + 1 });
});
it('should add a checkmark to a selected restricted FROM time', async () => {
createComponent({
props: {
form: {
...formEmptyState,
isRestrictedToTime: true,
restrictedTo: { startTime: timeFrom, endTime: timeTo },
},
},
},
});
expect(
findRestrictedFromOptions()
.at(timeFrom - 1)
.props('isChecked'),
).toBe(true);
expect(
findRestrictedToOptions()
.at(timeTo - 1)
.props('isChecked'),
).toBe(true);
});
await wrapper.vm.$nextTick();
expect(
findRestrictedFromOptions()
.at(timeFrom - 1)
.props('isChecked'),
).toBe(true);
expect(
findRestrictedToOptions()
.at(timeTo - 1)
.props('isChecked'),
).toBe(true);
});
});
......@@ -260,8 +252,6 @@ describe('AddEditRotationForm', () => {
tokenSelector.vm.$emit('blur');
tokenSelector.vm.$emit('focus');
await waitForPromises();
expect(tokenSelector.props('dropdownItems')).toMatchObject(participants);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
......@@ -276,8 +266,6 @@ describe('AddEditRotationForm', () => {
const tokenSelector = findUserSelector();
tokenSelector.vm.$emit('blur');
await wrapper.vm.$nextTick();
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
......
import { GlModal, GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlAlert, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue';
import AddEditRotationModal, {
......@@ -18,6 +18,7 @@ import {
createRotationResponse,
createRotationResponseWithErrors,
} from '../../mocks/apollo_mock';
import mockRotation from '../../mocks/mock_rotation.json';
jest.mock('~/flash');
......@@ -102,6 +103,7 @@ describe('AddEditRotationModal', () => {
propsData: {
modalId: addRotationModalId,
schedule,
rotation: mockRotation[0],
},
apolloProvider: fakeApollo,
data() {
......
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