Commit 3adbdb7c authored by Mark Florian's avatar Mark Florian

Merge branch '262859-restrict-oncall-to-times' into 'master'

Restrict to time intervals for rotations

See merge request gitlab-org/gitlab!50621
parents 413d2a37 1c1ba73d
...@@ -39,6 +39,10 @@ export const i18n = { ...@@ -39,6 +39,10 @@ export const i18n = {
enableToggle: s__('OnCallSchedules|Enable end date'), enableToggle: s__('OnCallSchedules|Enable end date'),
title: __('Ends on'), title: __('Ends on'),
}, },
restrictToTime: {
enableToggle: s__('OnCallSchedules|Restrict to time intervals'),
title: s__('OnCallSchedules|For this rotation, on-call will be:'),
},
}, },
}; };
...@@ -91,6 +95,7 @@ export default { ...@@ -91,6 +95,7 @@ export default {
return { return {
participantsArr: [], participantsArr: [],
endDateEnabled: false, endDateEnabled: false,
restrictToTimeEnabled: false,
}; };
}, },
methods: { methods: {
...@@ -100,121 +105,125 @@ export default { ...@@ -100,121 +105,125 @@ export default {
</script> </script>
<template> <template>
<gl-form class="w-75 gl-xs-w-full!" @submit.prevent="createRotation"> <gl-form @submit.prevent="createRotation">
<gl-form-group <div class="w-75 gl-xs-w-full!">
:label="$options.i18n.fields.name.title" <gl-form-group
label-size="sm" :label="$options.i18n.fields.name.title"
label-for="rotation-name" label-size="sm"
:invalid-feedback="$options.i18n.fields.name.error" label-for="rotation-name"
:state="validationState.name" :invalid-feedback="$options.i18n.fields.name.error"
> :state="validationState.name"
<gl-form-input
id="rotation-name"
@blur="$emit('update-rotation-form', { type: 'name', value: $event.target.value })"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.participants.title"
label-size="sm"
label-for="rotation-participants"
:invalid-feedback="$options.i18n.fields.participants.error"
:state="validationState.participants"
>
<gl-token-selector
v-model="participantsArr"
:dropdown-items="participants"
:loading="isLoading"
container-class="gl-h-13! gl-overflow-y-auto"
@text-input="$emit('filter-participants', $event)"
@blur="$emit('update-rotation-form', { type: 'participants', value: participantsArr })"
@input="$emit('update-rotation-form', { type: 'participants', value: participantsArr })"
> >
<template #token-content="{ token }">
<gl-avatar v-if="token.avatarUrl" :src="token.avatarUrl" :size="16" />
{{ token.name }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<gl-avatar-labeled
:src="dropdownItem.avatarUrl"
:size="32"
:label="dropdownItem.name"
:sub-label="dropdownItem.username"
/>
</template>
</gl-token-selector>
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.rotationLength.title"
label-size="sm"
label-for="rotation-length"
>
<div class="gl-display-flex">
<gl-form-input <gl-form-input
id="rotation-length" id="rotation-name"
type="number" @blur="$emit('update-rotation-form', { type: 'name', value: $event.target.value })"
class="gl-w-12 gl-mr-3"
min="1"
:value="1"
@input="$emit('update-rotation-form', { type: 'rotationLength.length', value: $event })"
/> />
<gl-dropdown :text="form.rotationLength.unit.toLowerCase()"> </gl-form-group>
<gl-dropdown-item
v-for="unit in $options.LENGTH_ENUM"
:key="unit"
:is-checked="form.rotationLength.unit === unit"
is-check-item
@click="$emit('update-rotation-form', { type: 'rotationLength.unit', value: unit })"
>
{{ unit.toLowerCase() }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</gl-form-group>
<gl-form-group <gl-form-group
:label="$options.i18n.fields.startsAt.title" :label="$options.i18n.fields.participants.title"
label-size="sm" label-size="sm"
label-for="rotation-start-time" label-for="rotation-participants"
:invalid-feedback="$options.i18n.fields.startsAt.error" :invalid-feedback="$options.i18n.fields.participants.error"
:state="validationState.startsAt" :state="validationState.participants"
> >
<div class="gl-display-flex gl-align-items-center"> <gl-token-selector
<gl-datepicker v-model="participantsArr"
class="gl-mr-3" :dropdown-items="participants"
@input="$emit('update-rotation-form', { type: 'startsAt.date', value: $event })" :loading="isLoading"
container-class="gl-h-13! gl-overflow-y-auto"
@text-input="$emit('filter-participants', $event)"
@blur="$emit('update-rotation-form', { type: 'participants', value: participantsArr })"
@input="$emit('update-rotation-form', { type: 'participants', value: participantsArr })"
> >
<template #default="{ formattedDate }"> <template #token-content="{ token }">
<gl-form-input <gl-avatar v-if="token.avatarUrl" :src="token.avatarUrl" :size="16" />
class="gl-w-full" {{ token.name }}
:value="formattedDate" </template>
:placeholder="__(`YYYY-MM-DD`)" <template #dropdown-item-content="{ dropdownItem }">
@blur=" <gl-avatar-labeled
$emit('update-rotation-form', { type: 'startsAt.date', value: $event.target.value }) :src="dropdownItem.avatarUrl"
" :size="32"
:label="dropdownItem.name"
:sub-label="dropdownItem.username"
/> />
</template> </template>
</gl-datepicker> </gl-token-selector>
<span> {{ __('at') }} </span> </gl-form-group>
<gl-dropdown
id="rotation-start-time" <gl-form-group
:text="format24HourTimeStringFromInt(form.startsAt.time)" :label="$options.i18n.fields.rotationLength.title"
class="gl-w-12 gl-pl-3" label-size="sm"
> label-for="rotation-length"
<gl-dropdown-item >
v-for="time in $options.HOURS_IN_DAY" <div class="gl-display-flex">
:key="time" <gl-form-input
:is-checked="form.startsAt.time === time" id="rotation-length"
is-check-item type="number"
@click="$emit('update-rotation-form', { type: 'startsAt.time', value: time })" class="gl-w-12 gl-mr-3"
min="1"
:value="1"
@input="$emit('update-rotation-form', { type: 'rotationLength.length', value: $event })"
/>
<gl-dropdown :text="form.rotationLength.unit.toLowerCase()">
<gl-dropdown-item
v-for="unit in $options.LENGTH_ENUM"
:key="unit"
:is-checked="form.rotationLength.unit === unit"
is-check-item
@click="$emit('update-rotation-form', { type: 'rotationLength.unit', value: unit })"
>
{{ unit.toLowerCase() }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.startsAt.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.startsAt.error"
:state="validationState.startsAt"
>
<div class="gl-display-flex gl-align-items-center">
<gl-datepicker
class="gl-mr-3"
@input="$emit('update-rotation-form', { type: 'startsAt.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: 'startsAt.date',
value: $event.target.value,
})
"
/>
</template>
</gl-datepicker>
<span> {{ __('at') }} </span>
<gl-dropdown
data-testid="rotation-start-time"
:text="format24HourTimeStringFromInt(form.startsAt.time)"
class="gl-w-12 gl-pl-3"
> >
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span> <gl-dropdown-item
</gl-dropdown-item> v-for="time in $options.HOURS_IN_DAY"
</gl-dropdown> :key="time"
<span class="gl-pl-5"> {{ schedule.timezone }} </span> :is-checked="form.startsAt.time === time"
</div> is-check-item
</gl-form-group> @click="$emit('update-rotation-form', { type: 'startsAt.time', value: time })"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span class="gl-pl-5"> {{ schedule.timezone }} </span>
</div>
</gl-form-group>
</div>
<gl-toggle <gl-toggle
v-model="endDateEnabled" v-model="endDateEnabled"
...@@ -223,11 +232,10 @@ export default { ...@@ -223,11 +232,10 @@ export default {
class="gl-mb-5" class="gl-mb-5"
/> />
<gl-card v-if="endDateEnabled" class="gl-min-w-fit-content" data-testid="rotation-ends-on"> <gl-card v-if="endDateEnabled" data-testid="rotation-ends-on">
<gl-form-group <gl-form-group
:label="$options.i18n.fields.endsOn.title" :label="$options.i18n.fields.endsOn.title"
label-size="sm" label-size="sm"
label-for="rotation-end-time"
:invalid-feedback="$options.i18n.fields.endsOn.error" :invalid-feedback="$options.i18n.fields.endsOn.error"
> >
<div class="gl-display-flex gl-align-items-center"> <div class="gl-display-flex gl-align-items-center">
...@@ -237,7 +245,7 @@ export default { ...@@ -237,7 +245,7 @@ export default {
/> />
<span> {{ __('at') }} </span> <span> {{ __('at') }} </span>
<gl-dropdown <gl-dropdown
id="rotation-end-time" data-testid="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)" :text="format24HourTimeStringFromInt(form.endsOn.time)"
class="gl-w-12 gl-pl-3" class="gl-w-12 gl-pl-3"
> >
...@@ -255,5 +263,56 @@ export default { ...@@ -255,5 +263,56 @@ export default {
</div> </div>
</gl-form-group> </gl-form-group>
</gl-card> </gl-card>
<gl-toggle
v-model="restrictToTimeEnabled"
data-testid="restricted-to-toggle"
:label="$options.i18n.fields.restrictToTime.enableToggle"
label-position="left"
class="gl-my-5"
/>
<gl-card v-if="restrictToTimeEnabled" data-testid="restricted-to-time">
<gl-form-group
:label="$options.i18n.fields.restrictToTime.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
>
<div class="gl-display-flex gl-align-items-center">
<span> {{ __('From') }} </span>
<gl-dropdown
data-testid="restricted-from"
:text="format24HourTimeStringFromInt(form.restrictedTo.from)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.from === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.from', value: time })"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span> {{ __('To') }} </span>
<gl-dropdown
data-testid="restricted-to"
:text="format24HourTimeStringFromInt(form.restrictedTo.to)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.to === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.to', value: time })"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
</div>
</gl-form-group>
</gl-card>
</gl-form> </gl-form>
</template> </template>
...@@ -84,6 +84,10 @@ export default { ...@@ -84,6 +84,10 @@ export default {
date: null, date: null,
time: 0, time: 0,
}, },
restrictedTo: {
from: 0,
to: 0,
},
}, },
error: '', error: '',
validationState: { validationState: {
......
...@@ -44,6 +44,10 @@ describe('AddEditRotationForm', () => { ...@@ -44,6 +44,10 @@ describe('AddEditRotationForm', () => {
date: null, date: null,
time: 0, time: 0,
}, },
restrictedTo: {
from: 0,
to: 0,
},
}, },
}, },
provide: { provide: {
...@@ -63,14 +67,20 @@ describe('AddEditRotationForm', () => { ...@@ -63,14 +67,20 @@ describe('AddEditRotationForm', () => {
}); });
const findRotationLength = () => wrapper.find('[id="rotation-length"]'); const findRotationLength = () => wrapper.find('[id="rotation-length"]');
const findRotationStartTime = () => wrapper.find('[id="rotation-start-time"]'); const findRotationStartTime = () => wrapper.find('[data-testid="rotation-start-time"]');
const findRotationEndsContainer = () => wrapper.find('[data-testid="rotation-ends-on"]'); const findRotationEndsContainer = () => wrapper.find('[data-testid="rotation-ends-on"]');
const findEndDateToggle = () => wrapper.find(GlToggle); const findEndDateToggle = () => wrapper.find(GlToggle);
const findRotationEndTime = () => wrapper.find('[id="rotation-end-time"]'); const findRotationEndTime = () => wrapper.find('[data-testid="rotation-end-time"]');
const findUserSelector = () => wrapper.find(GlTokenSelector); const findUserSelector = () => wrapper.find(GlTokenSelector);
const findRotationFormGroups = () => wrapper.findAllComponents(GlFormGroup); const findRotationFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findStartsOnTimeOptions = () => findRotationStartTime().findAllComponents(GlDropdownItem); const findStartsOnTimeOptions = () => findRotationStartTime().findAllComponents(GlDropdownItem);
const findEndsOnTimeOptions = () => findRotationEndTime().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);
const findRestrictedToOptions = () =>
wrapper.find('[data-testid="restricted-to"]').findAllComponents(GlDropdownItem);
describe('Rotation form validation', () => { describe('Rotation form validation', () => {
it.each` it.each`
...@@ -179,6 +189,68 @@ describe('AddEditRotationForm', () => { ...@@ -179,6 +189,68 @@ describe('AddEditRotationForm', () => {
}); });
}); });
describe('Rotation restricted to time', () => {
it('toggles restricted to time visibility', async () => {
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);
});
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();
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 });
});
it('should add a checkmark to a selected restricted FROM time', async () => {
findRestrictedToToggle().vm.$emit('change', true);
const timeFrom = 5;
const timeTo = 22;
wrapper.setProps({
form: {
endsOn: {
time: 0,
},
startsAt: {
time: 0,
},
restrictedTo: {
from: timeFrom,
to: timeTo,
},
rotationLength: {
length: 1,
unit: LENGTH_ENUM.hours,
},
},
});
await wrapper.vm.$nextTick();
expect(
findRestrictedFromOptions()
.at(timeFrom - 1)
.props('isChecked'),
).toBe(true);
expect(
findRestrictedToOptions()
.at(timeTo - 1)
.props('isChecked'),
).toBe(true);
});
});
describe('filter participants', () => { describe('filter participants', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
......
...@@ -19609,12 +19609,18 @@ msgstr "" ...@@ -19609,12 +19609,18 @@ msgstr ""
msgid "OnCallSchedules|Failed to edit schedule" msgid "OnCallSchedules|Failed to edit schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|For this rotation, on-call will be:"
msgstr ""
msgid "OnCallSchedules|On-call schedule" msgid "OnCallSchedules|On-call schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|On-call schedule for the %{timezone}" msgid "OnCallSchedules|On-call schedule for the %{timezone}"
msgstr "" msgstr ""
msgid "OnCallSchedules|Restrict to time intervals"
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