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

Merge branch '262858-end-datetime-for-rotations' into 'master'

Enable end date/time for rotations

See merge request gitlab-org/gitlab!50327
parents 851161ad 7afd4ad1
...@@ -29,6 +29,18 @@ ...@@ -29,6 +29,18 @@
} }
} }
.rotations-modal {
.gl-card {
min-width: 75%;
width: fit-content;
@include gl-bg-gray-10;
}
&.gl-modal .modal-md {
max-width: 640px;
}
}
//// Copied from roadmaps.scss - adapted for on-call schedules //// Copied from roadmaps.scss - adapted for on-call schedules
$header-item-height: 72px; $header-item-height: 72px;
$item-height: 40px; $item-height: 40px;
......
...@@ -9,6 +9,8 @@ import { ...@@ -9,6 +9,8 @@ import {
GlTokenSelector, GlTokenSelector,
GlAvatar, GlAvatar,
GlAvatarLabeled, GlAvatarLabeled,
GlToggle,
GlCard,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { import {
LENGTH_ENUM, LENGTH_ENUM,
...@@ -33,6 +35,10 @@ export const i18n = { ...@@ -33,6 +35,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: {
enableToggle: s__('OnCallSchedules|Enable end date'),
title: __('Ends on'),
},
}, },
}; };
...@@ -55,6 +61,8 @@ export default { ...@@ -55,6 +61,8 @@ export default {
GlTokenSelector, GlTokenSelector,
GlAvatar, GlAvatar,
GlAvatarLabeled, GlAvatarLabeled,
GlToggle,
GlCard,
}, },
props: { props: {
form: { form: {
...@@ -82,6 +90,7 @@ export default { ...@@ -82,6 +90,7 @@ export default {
data() { data() {
return { return {
participantsArr: [], participantsArr: [],
endDateEnabled: false,
}; };
}, },
methods: { methods: {
...@@ -150,7 +159,7 @@ export default { ...@@ -150,7 +159,7 @@ export default {
:value="1" :value="1"
@input="$emit('update-rotation-form', { type: 'rotationLength.length', value: $event })" @input="$emit('update-rotation-form', { type: 'rotationLength.length', value: $event })"
/> />
<gl-dropdown id="rotation-length" :text="form.rotationLength.unit.toLowerCase()"> <gl-dropdown :text="form.rotationLength.unit.toLowerCase()">
<gl-dropdown-item <gl-dropdown-item
v-for="unit in $options.LENGTH_ENUM" v-for="unit in $options.LENGTH_ENUM"
:key="unit" :key="unit"
...@@ -167,7 +176,7 @@ export default { ...@@ -167,7 +176,7 @@ export default {
<gl-form-group <gl-form-group
:label="$options.i18n.fields.startsAt.title" :label="$options.i18n.fields.startsAt.title"
label-size="sm" label-size="sm"
label-for="rotation-time" label-for="rotation-start-time"
:invalid-feedback="$options.i18n.fields.startsAt.error" :invalid-feedback="$options.i18n.fields.startsAt.error"
:state="validationState.startsAt" :state="validationState.startsAt"
> >
...@@ -189,7 +198,7 @@ export default { ...@@ -189,7 +198,7 @@ export default {
</gl-datepicker> </gl-datepicker>
<span> {{ __('at') }} </span> <span> {{ __('at') }} </span>
<gl-dropdown <gl-dropdown
id="rotation-time" id="rotation-start-time"
:text="format24HourTimeStringFromInt(form.startsAt.time)" :text="format24HourTimeStringFromInt(form.startsAt.time)"
class="gl-w-12 gl-pl-3" class="gl-w-12 gl-pl-3"
> >
...@@ -206,5 +215,45 @@ export default { ...@@ -206,5 +215,45 @@ export default {
<span class="gl-pl-5"> {{ schedule.timezone }} </span> <span class="gl-pl-5"> {{ schedule.timezone }} </span>
</div> </div>
</gl-form-group> </gl-form-group>
<gl-toggle
v-model="endDateEnabled"
:label="$options.i18n.fields.endsOn.enableToggle"
label-position="left"
class="gl-mb-5"
/>
<gl-card v-if="endDateEnabled" class="gl-min-w-fit-content" data-testid="rotation-ends-on">
<gl-form-group
:label="$options.i18n.fields.endsOn.title"
label-size="sm"
label-for="rotation-end-time"
:invalid-feedback="$options.i18n.fields.endsOn.error"
>
<div class="gl-display-flex gl-align-items-center">
<gl-datepicker
class="gl-mr-3"
@input="$emit('update-rotation-form', { type: 'endsOn.date', value: $event })"
/>
<span> {{ __('at') }} </span>
<gl-dropdown
id="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)"
class="gl-w-12 gl-pl-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.endsOn.time === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'endsOn.time', value: time })"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<div class="gl-mx-5">{{ schedule.timezone }}</div>
</div>
</gl-form-group>
</gl-card>
</gl-form> </gl-form>
</template> </template>
...@@ -80,6 +80,10 @@ export default { ...@@ -80,6 +80,10 @@ export default {
date: null, date: null,
time: 0, time: 0,
}, },
endsOn: {
date: null,
time: 0,
},
}, },
error: '', error: '',
validationState: { validationState: {
...@@ -241,10 +245,10 @@ export default { ...@@ -241,10 +245,10 @@ export default {
<gl-modal <gl-modal
ref="addEditScheduleRotationModal" ref="addEditScheduleRotationModal"
:modal-id="modalId" :modal-id="modalId"
size="sm"
:title="title" :title="title"
:action-primary="actionsProps.primary" :action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel" :action-cancel="actionsProps.cancel"
modal-class="rotations-modal"
@primary.prevent="isEditMode ? editRotation() : createRotation()" @primary.prevent="isEditMode ? editRotation() : createRotation()"
> >
<gl-alert v-if="error" variant="danger" @dismiss="error = ''"> <gl-alert v-if="error" variant="danger" @dismiss="error = ''">
......
...@@ -5,9 +5,9 @@ exports[`AddEditRotationModal renders rotation modal layout 1`] = ` ...@@ -5,9 +5,9 @@ exports[`AddEditRotationModal renders rotation modal layout 1`] = `
actioncancel="[object Object]" actioncancel="[object Object]"
actionprimary="[object Object]" actionprimary="[object Object]"
dismisslabel="Close" dismisslabel="Close"
modalclass="" modalclass="rotations-modal"
modalid="addRotationModal" modalid="addRotationModal"
size="sm" size="md"
title="Add rotation" title="Add rotation"
titletag="h4" titletag="h4"
> >
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { GlDropdownItem, GlTokenSelector, GlFormGroup } from '@gitlab/ui'; import { GlDropdownItem, GlTokenSelector, GlFormGroup, GlToggle } from '@gitlab/ui';
import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue'; import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue';
import { LENGTH_ENUM } from 'ee/oncall_schedules/constants'; import { LENGTH_ENUM } from 'ee/oncall_schedules/constants';
import { participants, getOncallSchedulesQueryResponse } from '../../mocks/apollo_mock'; import { participants, getOncallSchedulesQueryResponse } from '../../mocks/apollo_mock';
...@@ -40,6 +40,10 @@ describe('AddEditRotationForm', () => { ...@@ -40,6 +40,10 @@ describe('AddEditRotationForm', () => {
date: null, date: null,
time: 0, time: 0,
}, },
endsOn: {
date: null,
time: 0,
},
}, },
}, },
provide: { provide: {
...@@ -53,14 +57,20 @@ describe('AddEditRotationForm', () => { ...@@ -53,14 +57,20 @@ describe('AddEditRotationForm', () => {
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); if (wrapper) {
wrapper.destroy();
}
}); });
const findRotationLength = () => wrapper.find('[id = "rotation-length"]'); const findRotationLength = () => wrapper.find('[id="rotation-length"]');
const findRotationStartsOn = () => wrapper.find('[id = "rotation-time"]'); const findRotationStartTime = () => wrapper.find('[id="rotation-start-time"]');
const findRotationEndsContainer = () => wrapper.find('[data-testid="rotation-ends-on"]');
const findEndDateToggle = () => wrapper.find(GlToggle);
const findRotationEndTime = () => wrapper.find('[id="rotation-end-time"]');
const findUserSelector = () => wrapper.find(GlTokenSelector); const findUserSelector = () => wrapper.find(GlTokenSelector);
const findDropdownOptions = () => wrapper.findAllComponents(GlDropdownItem);
const findRotationFormGroups = () => wrapper.findAllComponents(GlFormGroup); const findRotationFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findStartsOnTimeOptions = () => findRotationStartTime().findAllComponents(GlDropdownItem);
const findEndsOnTimeOptions = () => findRotationEndTime().findAllComponents(GlDropdownItem);
describe('Rotation form validation', () => { describe('Rotation form validation', () => {
it.each` it.each`
...@@ -85,19 +95,87 @@ describe('AddEditRotationForm', () => { ...@@ -85,19 +95,87 @@ describe('AddEditRotationForm', () => {
}); });
it('renders the rotation starts on datepicker', async () => { it('renders the rotation starts on datepicker', async () => {
const startsOn = findRotationStartsOn(); const startsOn = findRotationStartTime();
expect(startsOn.exists()).toBe(true); expect(startsOn.exists()).toBe(true);
expect(startsOn.attributes('text')).toBe('00:00'); expect(startsOn.attributes('text')).toBe('00:00');
expect(startsOn.attributes('headertext')).toBe(''); expect(startsOn.attributes('headertext')).toBe('');
}); });
it('should add a check for a rotation length type selected', async () => { it('should emit an event with selected value on time selection', async () => {
const selectedLengthType1 = findDropdownOptions().at(0); findStartsOnTimeOptions().at(3).vm.$emit('click');
const selectedLengthType2 = findDropdownOptions().at(1); await wrapper.vm.$nextTick();
selectedLengthType1.vm.$emit('click'); const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'startsAt.time', value: 4 });
});
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,
},
},
});
await wrapper.vm.$nextTick();
expect(
findStartsOnTimeOptions()
.at(time - 1)
.props('isChecked'),
).toBe(true);
});
});
describe('Rotation end time', () => {
it('toggles end time visibility', async () => {
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();
expect(findRotationEndsContainer().exists()).toBe(true);
});
it('should emit an event with selected value on time selection', async () => {
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: 'endsOn.time', value: option + 1 });
});
it('should add a checkmark to a selected end time', async () => {
findEndDateToggle().vm.$emit('change', true);
const time = 5;
wrapper.setProps({
form: {
endsOn: {
time,
},
startsAt: {
time: 0,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
},
},
});
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(selectedLengthType1.props('isChecked')).toBe(true); expect(
expect(selectedLengthType2.props('isChecked')).toBe(false); findEndsOnTimeOptions()
.at(time - 1)
.props('isChecked'),
).toBe(true);
}); });
}); });
......
...@@ -10758,6 +10758,9 @@ msgstr "" ...@@ -10758,6 +10758,9 @@ msgstr ""
msgid "Ends at (UTC)" msgid "Ends at (UTC)"
msgstr "" msgstr ""
msgid "Ends on"
msgstr ""
msgid "Enforce DNS rebinding attack protection" msgid "Enforce DNS rebinding attack protection"
msgstr "" msgstr ""
...@@ -19591,6 +19594,9 @@ msgstr "" ...@@ -19591,6 +19594,9 @@ msgstr ""
msgid "OnCallSchedules|Edit schedule" msgid "OnCallSchedules|Edit schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|Enable end date"
msgstr ""
msgid "OnCallSchedules|Failed to add rotation" msgid "OnCallSchedules|Failed to add rotation"
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