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 = {
enableToggle: s__('OnCallSchedules|Enable end date'),
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 {
return {
participantsArr: [],
endDateEnabled: false,
restrictToTimeEnabled: false,
};
},
methods: {
......@@ -100,121 +105,125 @@ export default {
</script>
<template>
<gl-form class="w-75 gl-xs-w-full!" @submit.prevent="createRotation">
<gl-form-group
:label="$options.i18n.fields.name.title"
label-size="sm"
label-for="rotation-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 })"
<gl-form @submit.prevent="createRotation">
<div class="w-75 gl-xs-w-full!">
<gl-form-group
:label="$options.i18n.fields.name.title"
label-size="sm"
label-for="rotation-name"
:invalid-feedback="$options.i18n.fields.name.error"
:state="validationState.name"
>
<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
id="rotation-length"
type="number"
class="gl-w-12 gl-mr-3"
min="1"
:value="1"
@input="$emit('update-rotation-form', { type: 'rotationLength.length', value: $event })"
id="rotation-name"
@blur="$emit('update-rotation-form', { type: 'name', value: $event.target.value })"
/>
<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>
<gl-form-group
:label="$options.i18n.fields.startsAt.title"
label-size="sm"
label-for="rotation-start-time"
: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 })"
<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 #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 #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-datepicker>
<span> {{ __('at') }} </span>
<gl-dropdown
id="rotation-start-time"
:text="format24HourTimeStringFromInt(form.startsAt.time)"
class="gl-w-12 gl-pl-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.startsAt.time === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'startsAt.time', value: time })"
</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
id="rotation-length"
type="number"
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>
<span class="gl-pl-5"> {{ schedule.timezone }} </span>
</div>
</gl-form-group>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.startsAt.time === time"
is-check-item
@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
v-model="endDateEnabled"
......@@ -223,11 +232,10 @@ export default {
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
: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">
......@@ -237,7 +245,7 @@ export default {
/>
<span> {{ __('at') }} </span>
<gl-dropdown
id="rotation-end-time"
data-testid="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)"
class="gl-w-12 gl-pl-3"
>
......@@ -255,5 +263,56 @@ export default {
</div>
</gl-form-group>
</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>
</template>
......@@ -84,6 +84,10 @@ export default {
date: null,
time: 0,
},
restrictedTo: {
from: 0,
to: 0,
},
},
error: '',
validationState: {
......
......@@ -44,6 +44,10 @@ describe('AddEditRotationForm', () => {
date: null,
time: 0,
},
restrictedTo: {
from: 0,
to: 0,
},
},
},
provide: {
......@@ -63,14 +67,20 @@ describe('AddEditRotationForm', () => {
});
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 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 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);
const findRestrictedToOptions = () =>
wrapper.find('[data-testid="restricted-to"]').findAllComponents(GlDropdownItem);
describe('Rotation form validation', () => {
it.each`
......@@ -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', () => {
beforeEach(() => {
createComponent();
......
......@@ -19609,12 +19609,18 @@ msgstr ""
msgid "OnCallSchedules|Failed to edit schedule"
msgstr ""
msgid "OnCallSchedules|For this rotation, on-call will be:"
msgstr ""
msgid "OnCallSchedules|On-call schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule for the %{timezone}"
msgstr ""
msgid "OnCallSchedules|Restrict to time intervals"
msgstr ""
msgid "OnCallSchedules|Rotation length"
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