Commit 80c02ae6 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Phil Hughes

Create rotation with endsAt date - integration

parent f1954931
...@@ -40,9 +40,10 @@ export const i18n = { ...@@ -40,9 +40,10 @@ export const i18n = {
title: __('Starts on'), title: __('Starts on'),
error: s__('OnCallSchedules|Rotation start date cannot be empty'), error: s__('OnCallSchedules|Rotation start date cannot be empty'),
}, },
endsOn: { endsAt: {
enableToggle: s__('OnCallSchedules|Enable end date'), enableToggle: s__('OnCallSchedules|Enable end date'),
title: __('Ends on'), title: __('Ends on'),
error: s__('OnCallSchedules|Rotation end date/time must come after start date/time'),
}, },
restrictToTime: { restrictToTime: {
enableToggle: s__('OnCallSchedules|Restrict to time intervals'), enableToggle: s__('OnCallSchedules|Restrict to time intervals'),
...@@ -234,7 +235,7 @@ export default { ...@@ -234,7 +235,7 @@ export default {
<div class="gl-display-inline-block"> <div class="gl-display-inline-block">
<gl-toggle <gl-toggle
v-model="endDateEnabled" v-model="endDateEnabled"
:label="$options.i18n.fields.endsOn.enableToggle" :label="$options.i18n.fields.endsAt.enableToggle"
label-position="left" label-position="left"
class="gl-mb-5" class="gl-mb-5"
/> />
...@@ -245,28 +246,43 @@ export default { ...@@ -245,28 +246,43 @@ export default {
class="gl-border-gray-400 gl-bg-gray-10" class="gl-border-gray-400 gl-bg-gray-10"
> >
<gl-form-group <gl-form-group
:label="$options.i18n.fields.endsOn.title" :label="$options.i18n.fields.endsAt.title"
label-size="sm" label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error" :state="validationState.endsAt"
:invalid-feedback="$options.i18n.fields.endsAt.error"
class="gl-mb-0" class="gl-mb-0"
> >
<div class="gl-display-flex gl-align-items-center"> <div class="gl-display-flex gl-align-items-center">
<gl-datepicker <gl-datepicker
class="gl-mr-3" class="gl-mr-3"
@input="$emit('update-rotation-form', { type: 'endsOn.date', value: $event })" @input="$emit('update-rotation-form', { type: 'endsAt.date', value: $event })"
>
<template #default="{ formattedDate }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
@blur="
$emit('update-rotation-form', {
type: 'endsAt.date',
value: $event.target.value,
})
"
/> />
</template>
</gl-datepicker>
<span> {{ __('at') }} </span> <span> {{ __('at') }} </span>
<gl-dropdown <gl-dropdown
data-testid="rotation-end-time" data-testid="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)" :text="format24HourTimeStringFromInt(form.endsAt.time)"
class="gl-px-3" class="gl-px-3"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY" v-for="time in $options.HOURS_IN_DAY"
:key="time" :key="time"
:is-checked="form.endsOn.time === time" :is-checked="form.endsAt.time === time"
is-check-item is-check-item
@click="$emit('update-rotation-form', { type: 'endsOn.time', value: time })" @click="$emit('update-rotation-form', { type: 'endsAt.time', value: time })"
> >
<span class="gl-white-space-nowrap"> <span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span {{ format24HourTimeStringFromInt(time) }}</span
...@@ -294,7 +310,7 @@ export default { ...@@ -294,7 +310,7 @@ export default {
<gl-form-group <gl-form-group
:label="$options.i18n.fields.restrictToTime.title" :label="$options.i18n.fields.restrictToTime.title"
label-size="sm" label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error" :invalid-feedback="$options.i18n.fields.endsAt.error"
class="gl-mb-0" class="gl-mb-0"
> >
<div class="gl-display-flex gl-align-items-center"> <div class="gl-display-flex gl-align-items-center">
......
...@@ -78,7 +78,7 @@ export default { ...@@ -78,7 +78,7 @@ export default {
date: null, date: null,
time: 0, time: 0,
}, },
endsOn: { endsAt: {
date: null, date: null,
time: 0, time: 0,
}, },
...@@ -92,6 +92,7 @@ export default { ...@@ -92,6 +92,7 @@ export default {
name: true, name: true,
participants: true, participants: true,
startsAt: true, startsAt: true,
endsAt: true,
}, },
}; };
}, },
...@@ -129,7 +130,8 @@ export default { ...@@ -129,7 +130,8 @@ export default {
name, name,
rotationLength, rotationLength,
participants, participants,
startsAt: { date, time }, startsAt: { date: startDate, time: startTime },
endsAt: { date: endDate, time: endTime },
} = this.form; } = this.form;
return { return {
...@@ -137,9 +139,15 @@ export default { ...@@ -137,9 +139,15 @@ export default {
scheduleIid: this.schedule.iid, scheduleIid: this.schedule.iid,
name, name,
startsAt: { startsAt: {
date: formatDate(date, 'yyyy-mm-dd'), date: formatDate(startDate, 'yyyy-mm-dd'),
time: format24HourTimeStringFromInt(time), time: format24HourTimeStringFromInt(startTime),
}, },
endsAt: endDate
? {
date: formatDate(endDate, 'yyyy-mm-dd'),
time: format24HourTimeStringFromInt(endTime),
}
: null,
rotationLength: { rotationLength: {
...rotationLength, ...rotationLength,
length: parseInt(rotationLength.length, 10), length: parseInt(rotationLength.length, 10),
...@@ -150,6 +158,20 @@ export default { ...@@ -150,6 +158,20 @@ export default {
title() { title() {
return this.isEditMode ? this.$options.i18n.editRotation : this.$options.i18n.addRotation; return this.isEditMode ? this.$options.i18n.editRotation : this.$options.i18n.addRotation;
}, },
isEndDateValid() {
const startsAt = this.form.startsAt.date?.getTime();
const endsAt = this.form.endsAt.date?.getTime();
if (!startsAt || !endsAt) {
// If start or end is not present, we consider the end date valid
return true;
} else if (startsAt < endsAt) {
return true;
} else if (startsAt === endsAt) {
return this.form.startsAt.time < this.form.endsAt.time;
}
return false;
},
}, },
methods: { methods: {
createRotation() { createRotation() {
...@@ -244,8 +266,11 @@ export default { ...@@ -244,8 +266,11 @@ export default {
this.validationState.name = isNameFieldValid(this.form.name); this.validationState.name = isNameFieldValid(this.form.name);
} else if (key === 'participants') { } else if (key === 'participants') {
this.validationState.participants = this.form.participants.length > 0; this.validationState.participants = this.form.participants.length > 0;
} else if (key === 'startsAt.date') { } else if (key === 'startsAt.date' || key === 'startsAt.time') {
this.validationState.startsAt = Boolean(this.form.startsAt.date); this.validationState.startsAt = Boolean(this.form.startsAt.date);
this.validationState.endsAt = this.isEndDateValid;
} else if (key === 'endsAt.date' || key === 'endsAt.time') {
this.validationState.endsAt = this.isEndDateValid;
} }
}, },
}, },
......
...@@ -4,6 +4,7 @@ fragment OnCallRotation on IncidentManagementOncallRotation { ...@@ -4,6 +4,7 @@ fragment OnCallRotation on IncidentManagementOncallRotation {
id id
name name
startsAt startsAt
endsAt
length length
lengthUnit lengthUnit
participants { participants {
......
...@@ -138,7 +138,8 @@ export const createRotationResponse = { ...@@ -138,7 +138,8 @@ export const createRotationResponse = {
oncallRotation: { oncallRotation: {
id: '44', id: '44',
name: 'Test', name: 'Test',
startsAt: '2020-12-17T12:00:00Z', startsAt: '2020-12-20T12:00:00Z',
endsAt: '2021-03-17T12:00:00Z',
length: 5, length: 5,
lengthUnit: 'WEEKS', lengthUnit: 'WEEKS',
participants: { participants: {
...@@ -171,7 +172,8 @@ export const createRotationResponseWithErrors = { ...@@ -171,7 +172,8 @@ export const createRotationResponseWithErrors = {
oncallRotation: { oncallRotation: {
id: '44', id: '44',
name: 'Test', name: 'Test',
startsAt: '2020-12-17T12:00:00Z', startsAt: '2020-12-20T12:00:00Z',
endsAt: '2021-03-17T12:00:00Z',
length: 5, length: 5,
lengthUnit: 'WEEKS', lengthUnit: 'WEEKS',
participants: { participants: {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/2", "id": "gid://gitlab/IncidentManagement::OncallRotation/2",
"name": "Rotation 242", "name": "Rotation 242",
"startsAt": "2021-01-13T10:04:56.333Z", "startsAt": "2021-01-13T10:04:56.333Z",
"endsAt": "2021-03-13T10:04:56.333Z",
"length": 1, "length": 1,
"lengthUnit": "WEEKS", "lengthUnit": "WEEKS",
"participants": { "participants": {
...@@ -54,6 +55,7 @@ ...@@ -54,6 +55,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/55", "id": "gid://gitlab/IncidentManagement::OncallRotation/55",
"name": "Rotation 242", "name": "Rotation 242",
"startsAt": "2021-01-13T10:04:56.333Z", "startsAt": "2021-01-13T10:04:56.333Z",
"endsAt": "2021-03-13T10:04:56.333Z",
"length": 1, "length": 1,
"lengthUnit": "WEEKS", "lengthUnit": "WEEKS",
"participants": { "participants": {
...@@ -102,6 +104,7 @@ ...@@ -102,6 +104,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/3", "id": "gid://gitlab/IncidentManagement::OncallRotation/3",
"name": "Rotation 244", "name": "Rotation 244",
"startsAt": "2021-01-06T10:04:56.333Z", "startsAt": "2021-01-06T10:04:56.333Z",
"endsAt": "2021-01-10T10:04:56.333Z",
"length": 1, "length": 1,
"lengthUnit": "WEEKS", "lengthUnit": "WEEKS",
"participants": { "participants": {
...@@ -150,6 +153,7 @@ ...@@ -150,6 +153,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/5", "id": "gid://gitlab/IncidentManagement::OncallRotation/5",
"name": "Rotation 247", "name": "Rotation 247",
"startsAt": "2021-01-06T10:04:56.333Z", "startsAt": "2021-01-06T10:04:56.333Z",
"endsAt": "2021-01-11T10:04:56.333Z",
"length": 1, "length": 1,
"lengthUnit": "WEEKS", "lengthUnit": "WEEKS",
"participants": { "participants": {
......
...@@ -40,7 +40,7 @@ describe('AddEditRotationForm', () => { ...@@ -40,7 +40,7 @@ describe('AddEditRotationForm', () => {
date: null, date: null,
time: 0, time: 0,
}, },
endsOn: { endsAt: {
date: null, date: null,
time: 0, time: 0,
}, },
...@@ -160,7 +160,7 @@ describe('AddEditRotationForm', () => { ...@@ -160,7 +160,7 @@ describe('AddEditRotationForm', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
const emittedEvent = wrapper.emitted('update-rotation-form'); const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(1); expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'endsOn.time', value: option + 1 }); expect(emittedEvent[0][0]).toEqual({ type: 'endsAt.time', value: option + 1 });
}); });
it('should add a checkmark to a selected end time', async () => { it('should add a checkmark to a selected end time', async () => {
...@@ -168,7 +168,7 @@ describe('AddEditRotationForm', () => { ...@@ -168,7 +168,7 @@ describe('AddEditRotationForm', () => {
const time = 5; const time = 5;
wrapper.setProps({ wrapper.setProps({
form: { form: {
endsOn: { endsAt: {
time, time,
}, },
startsAt: { startsAt: {
...@@ -221,7 +221,7 @@ describe('AddEditRotationForm', () => { ...@@ -221,7 +221,7 @@ describe('AddEditRotationForm', () => {
wrapper.setProps({ wrapper.setProps({
form: { form: {
endsOn: { endsAt: {
time: 0, time: 0,
}, },
startsAt: { startsAt: {
......
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue';
import AddEditRotationModal, { import AddEditRotationModal, {
i18n, i18n,
} from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue'; } from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue';
...@@ -129,8 +130,9 @@ describe('AddEditRotationModal', () => { ...@@ -129,8 +130,9 @@ describe('AddEditRotationModal', () => {
wrapper = null; wrapper = null;
}); });
const findModal = () => wrapper.find(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findForm = () => wrapper.findComponent(AddEditRotationForm);
it('renders rotation modal layout', () => { it('renders rotation modal layout', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
...@@ -155,6 +157,149 @@ describe('AddEditRotationModal', () => { ...@@ -155,6 +157,149 @@ describe('AddEditRotationModal', () => {
expect(findAlert().exists()).toBe(true); expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toContain(error); expect(findAlert().text()).toContain(error);
}); });
describe('Validation', () => {
describe('name', () => {
it('is valid when name is NOT empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: '' });
expect(form.props('validationState').name).toBe(false);
});
it('is NOT valid when name is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: 'Some value' });
expect(form.props('validationState').name).toBe(true);
});
});
describe('participants', () => {
it('is valid when participants array is NOT empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'participants',
value: ['user1', 'user2'],
});
expect(form.props('validationState').participants).toBe(true);
});
it('is NOT valid when participants array is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'participants', value: [] });
expect(form.props('validationState').participants).toBe(false);
});
});
describe('startsAt date', () => {
it('is valid when date is NOT empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('10/12/2021'),
});
expect(form.props('validationState').startsAt).toBe(true);
});
it('is NOT valid when date is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'startsAt.time', value: null });
expect(form.props('validationState').startsAt).toBe(false);
});
});
describe('endsAt date', () => {
it('is valid when date is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'endsAt.date', value: null });
expect(form.props('validationState').endsAt).toBe(true);
});
it('is valid when start date is smaller then end date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('9/11/2021'),
});
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('10/11/2021'),
});
expect(form.props('validationState').endsAt).toBe(true);
});
it('is invalid when start date is larger then end date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('10/11/2021'),
});
expect(form.props('validationState').endsAt).toBe(false);
});
it('is valid when start and end dates are equal but time is smaller on start date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'startsAt.time', value: 10 });
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'endsAt.time', value: 22 });
expect(form.props('validationState').endsAt).toBe(true);
});
it('is invalid when start and end dates are equal but time is larger on start date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'startsAt.time', value: 10 });
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'endsAt.time', value: 8 });
expect(form.props('validationState').endsAt).toBe(false);
});
});
describe('Toggle primary button state', () => {
it('should disable primary button when any of the fields is invalid', async () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: 'lalal' });
await wrapper.vm.$nextTick();
expect(findModal().props('actionPrimary').attributes).toEqual(
expect.arrayContaining([{ disabled: true }]),
);
});
it('should enable primary button when all fields are valid', async () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: 'Value' });
form.vm.$emit('update-rotation-form', { type: 'participants', value: [1, 2, 3] });
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/10/2021'),
});
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('12/10/2021'),
});
await wrapper.vm.$nextTick();
expect(findModal().props('actionPrimary').attributes).toEqual(
expect.arrayContaining([{ disabled: false }]),
);
});
});
});
}); });
describe('with mocked Apollo client', () => { describe('with mocked Apollo client', () => {
......
...@@ -20942,6 +20942,9 @@ msgstr "" ...@@ -20942,6 +20942,9 @@ msgstr ""
msgid "OnCallSchedules|Restrict to time intervals" msgid "OnCallSchedules|Restrict to time intervals"
msgstr "" msgstr ""
msgid "OnCallSchedules|Rotation end date/time must come after start date/time"
msgstr ""
msgid "OnCallSchedules|Rotation length" msgid "OnCallSchedules|Rotation length"
msgstr "" msgstr ""
......
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