Commit 680a9297 authored by David O'Regan's avatar David O'Regan Committed by Olena Horal-Koretska

Feat(oncallschedules): add rotation grid calucations

Allow rotation assignes to be calculated
and assigned to the rotation grid from
shift times
parent 757cb10d
...@@ -4,6 +4,8 @@ import * as timeago from 'timeago.js'; ...@@ -4,6 +4,8 @@ import * as timeago from 'timeago.js';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale'; import { languageCode, s__, __, n__ } from '../../locale';
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
window.timeago = timeago; window.timeago = timeago;
/** /**
...@@ -851,3 +853,45 @@ export const format24HourTimeStringFromInt = (time) => { ...@@ -851,3 +853,45 @@ export const format24HourTimeStringFromInt = (time) => {
const formatted24HourString = time > 9 ? `${time}:00` : `0${time}:00`; const formatted24HourString = time > 9 ? `${time}:00` : `0${time}:00`;
return formatted24HourString; return formatted24HourString;
}; };
/**
* A utility function which checks if two date ranges overlap.
*
* @param {Object} givenPeriodLeft - the first period to compare.
* @param {Object} givenPeriodRight - the second period to compare.
* @returns {Object} { overlap: number of days the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format }
* @throws {Error} Uncaught Error: Invalid period
*
* @example
* getOverlappingDaysInPeriods(
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) },
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) }
* ) => { daysOverlap: 2, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
*
*/
export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => {
const leftStartTime = new Date(givenPeriodLeft.start).getTime();
const leftEndTime = new Date(givenPeriodLeft.end).getTime();
const rightStartTime = new Date(givenPeriodRight.start).getTime();
const rightEndTime = new Date(givenPeriodRight.end).getTime();
if (!(leftStartTime <= leftEndTime && rightStartTime <= rightEndTime)) {
throw new Error(__('Invalid period'));
}
const isOverlapping = leftStartTime < rightEndTime && rightStartTime < leftEndTime;
if (!isOverlapping) {
return { daysOverlap: 0 };
}
const overlapStartDate = Math.max(leftStartTime, rightStartTime);
const overlapEndDate = rightEndTime > leftEndTime ? leftEndTime : rightEndTime;
const differenceInMs = overlapEndDate - overlapStartDate;
return {
daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY),
overlapStartDate,
overlapEndDate,
};
};
...@@ -113,8 +113,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi ...@@ -113,8 +113,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
} }
.item-label { .item-label {
@include gl-py-4;
@include gl-pl-4;
border-right: $border-style; border-right: $border-style;
border-bottom: $border-style; border-bottom: $border-style;
} }
......
<script> <script>
import { GlToken, GlAvatarLabeled, GlPopover } from '@gitlab/ui'; import { GlToken, GlAvatarLabeled, GlPopover } from '@gitlab/ui';
import { assigneeScheduleDateStart } from 'ee/oncall_schedules/utils/common_utils'; import { formatDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
...@@ -10,66 +10,65 @@ export default { ...@@ -10,66 +10,65 @@ export default {
GlPopover, GlPopover,
}, },
props: { props: {
assigneeIndex: {
type: Number,
required: true,
},
assignee: { assignee: {
type: Object, type: Object,
required: true, required: true,
}, },
rotationLength: { rotationAssigneeStartsAt: {
type: Number, type: String,
required: true, required: true,
}, },
rotationStartsAt: { rotationAssigneeEndsAt: {
type: String, type: String,
required: true, required: true,
}, },
rotationAssigneeStyle: {
type: Object,
required: true,
},
}, },
computed: { computed: {
chevronClass() { chevronClass() {
return `gl-bg-data-viz-${this.assignee.colorPalette}-${this.assignee.colorWeight}`; return `gl-bg-data-viz-${this.assignee.colorPalette}-${this.assignee.colorWeight}`;
}, },
startsAt() { startsAt() {
const startsAt = assigneeScheduleDateStart( return sprintf(__('Starts: %{startsAt}'), {
new Date(this.rotationStartsAt), startsAt: formatDate(this.rotationAssigneeStartsAt, 'mmmm d, yyyy, hh:mm'),
this.rotationLength * 7 * this.assigneeIndex, });
).toLocaleString();
return sprintf(__('Starts at %{startsAt}'), { startsAt });
}, },
endsAt() { endsAt() {
const endsAt = assigneeScheduleDateStart( return sprintf(__('Ends: %{endsAt}'), {
new Date(this.rotationStartsAt), endsAt: formatDate(this.rotationAssigneeEndsAt, 'mmmm d, yyyy, hh:mm'),
this.rotationLength * 7 * this.assigneeIndex + this.rotationLength * 7, });
).toLocaleString();
return sprintf(__('Ends at %{endsAt}'), { endsAt });
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-w-full gl-mt-3 gl-px-3"> <div
class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden"
:style="rotationAssigneeStyle"
>
<gl-token <gl-token
:id="assignee.user.id" :id="assignee.id"
class="gl-w-full gl-align-items-center" class="gl-w-full gl-h-6 gl-align-items-center"
:class="chevronClass" :class="chevronClass"
:view-only="true" :view-only="true"
> >
<gl-avatar-labeled <gl-avatar-labeled
shape="circle" shape="circle"
:size="16" :size="16"
:src="assignee.user.avatarUrl" :src="assignee.avatarUrl"
:label="assignee.user.username" :label="assignee.user.username"
:title="assignee.user.username" :title="assignee.user.username"
/> />
</gl-token> </gl-token>
<gl-popover <gl-popover
:target="assignee.user.id" :target="assignee.id"
:title="assignee.user.username" :title="assignee.user.username"
triggers="hover" triggers="hover"
placement="left" placement="top"
> >
<p class="gl-m-0" data-testid="rotation-assignee-starts-at">{{ startsAt }}</p> <p class="gl-m-0" data-testid="rotation-assignee-starts-at">{{ startsAt }}</p>
<p class="gl-m-0" data-testid="rotation-assignee-ends-at">{{ endsAt }}</p> <p class="gl-m-0" data-testid="rotation-assignee-ends-at">{{ endsAt }}</p>
......
...@@ -58,7 +58,11 @@ export default { ...@@ -58,7 +58,11 @@ export default {
<template> <template>
<span class="timeline-header-item"> <span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label" data-testid="timeline-header-label"> <div
:class="timelineHeaderClass"
class="item-label gl-pl-6 gl-py-4"
data-testid="timeline-header-label"
>
{{ timelineHeaderLabel }} {{ timelineHeaderLabel }}
</div> </div>
<weeks-header-sub-item :timeframe-item="timeframeItem" /> <weeks-header-sub-item :timeframe-item="timeframeItem" />
......
<script> <script>
import CommonMixin from '../../../../mixins/common_mixin'; import updateShiftTimeUnitWidthMutation from 'ee/oncall_schedules/graphql/mutations/update_shift_time_unit_width.mutation.graphql';
import CommonMixin from 'ee/oncall_schedules/mixins/common_mixin';
import { GlResizeObserverDirective } from '@gitlab/ui';
export default { export default {
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [CommonMixin], mixins: [CommonMixin],
props: { props: {
timeframeItem: { timeframeItem: {
...@@ -26,6 +31,9 @@ export default { ...@@ -26,6 +31,9 @@ export default {
return headerSubItems; return headerSubItems;
}, },
}, },
mounted() {
this.updateShiftStyles();
},
methods: { methods: {
getSubItemValueClass(subItem) { getSubItemValueClass(subItem) {
// Show dark color text only for current & upcoming dates // Show dark color text only for current & upcoming dates
...@@ -36,15 +44,28 @@ export default { ...@@ -36,15 +44,28 @@ export default {
} }
return ''; return '';
}, },
updateShiftStyles() {
this.$apollo.mutate({
mutation: updateShiftTimeUnitWidthMutation,
variables: {
shiftTimeUnitWidth: this.$refs.weeklyDayCell[0].offsetWidth,
},
});
},
}, },
}; };
</script> </script>
<template> <template>
<div class="item-sublabel"> <div
v-gl-resize-observer="updateShiftStyles"
class="item-sublabel"
data-testid="week-item-sublabel"
>
<span <span
v-for="(subItem, index) in headerSubItems" v-for="(subItem, index) in headerSubItems"
:key="index" :key="index"
ref="weeklyDayCell"
:class="getSubItemValueClass(subItem)" :class="getSubItemValueClass(subItem)"
class="sublabel-value" class="sublabel-value"
data-testid="sublabel-value" data-testid="sublabel-value"
......
<script> <script>
import { GlButtonGroup, GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; import { GlButtonGroup, GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import DeleteRotationModal from 'ee/oncall_schedules/components/rotations/components/delete_rotation_modal.vue';
import { editRotationModalId, deleteRotationModalId } from 'ee/oncall_schedules/constants';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import CurrentDayIndicator from './current_day_indicator.vue'; import CurrentDayIndicator from './current_day_indicator.vue';
import RotationAssignee from '../../rotations/components/rotation_assignee.vue'; import ScheduleShift from './schedule_shift.vue';
import DeleteRotationModal from '../../rotations/components/delete_rotation_modal.vue';
import { editRotationModalId, deleteRotationModalId } from '../../../constants';
export const i18n = { export const i18n = {
editRotationLabel: s__('OnCallSchedules|Edit rotation'), editRotationLabel: s__('OnCallSchedules|Edit rotation'),
...@@ -19,8 +19,8 @@ export default { ...@@ -19,8 +19,8 @@ export default {
GlButtonGroup, GlButtonGroup,
GlButton, GlButton,
CurrentDayIndicator, CurrentDayIndicator,
RotationAssignee,
DeleteRotationModal, DeleteRotationModal,
ScheduleShift,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -43,12 +43,16 @@ export default { ...@@ -43,12 +43,16 @@ export default {
data() { data() {
return { return {
rotationToUpdate: {}, rotationToUpdate: {},
shiftWidths: 0,
}; };
}, },
methods: { methods: {
setRotationToUpdate(rotation) { setRotationToUpdate(rotation) {
this.rotationToUpdate = rotation; this.rotationToUpdate = rotation;
}, },
isLastCell(index) {
return index + 1 === this.timeframe.length;
},
}, },
}; };
</script> </script>
...@@ -87,15 +91,19 @@ export default { ...@@ -87,15 +91,19 @@ export default {
<span <span
v-for="(timeframeItem, index) in timeframe" v-for="(timeframeItem, index) in timeframe"
:key="index" :key="index"
class="timeline-cell" class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:class="{ 'gl-overflow-hidden': isLastCell(index) }"
data-testid="timelineCell" data-testid="timelineCell"
> >
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" /> <current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<rotation-assignee <schedule-shift
:assignee="rotation.participants.nodes[index]" v-for="(shift, shiftIndex) in rotation.shifts.nodes"
:assignee-index="index" :key="shift.startAt"
:rotation-length="rotation.length" :shift="shift"
:rotation-starts-at="rotation.startsAt" :shift-index="shiftIndex"
:preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
/> />
</span> </span>
</div> </div>
......
<script>
import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import {
PRESET_TYPES,
DAYS_IN_WEEK,
DAYS_IN_DATE_WEEK,
ASSIGNEE_SPACER,
} from 'ee/oncall_schedules/constants';
import getShiftTimeUnitWidthQuery from 'ee/oncall_schedules/graphql/queries/get_shift_time_unit_width.query.graphql';
import { getOverlappingDaysInPeriods } from '~/lib/utils/datetime_utility';
import { incrementDateByDays } from '../utils';
export default {
components: {
RotationAssignee,
},
props: {
shift: {
type: Object,
required: true,
},
shiftIndex: {
type: Number,
required: true,
},
timeframeItem: {
type: [Date, Object],
required: true,
},
timeframe: {
type: Array,
required: true,
},
presetType: {
type: String,
required: true,
},
},
data() {
return {
shiftTimeUnitWidth: 0,
};
},
apollo: {
shiftTimeUnitWidth: {
query: getShiftTimeUnitWidthQuery,
},
},
computed: {
currentTimeframeEndsAt() {
let UnitOfIncrement = 0;
if (this.presetType === PRESET_TYPES.WEEKS) {
UnitOfIncrement = DAYS_IN_DATE_WEEK;
}
return incrementDateByDays(this.timeframeItem, UnitOfIncrement);
},
daysUntilEndOfTimeFrame() {
return (
this.currentTimeframeEndsAt.getDate() -
new Date(this.shiftRangeOverlap.overlapStartDate).getDate() +
1
);
},
rotationAssigneeStyle() {
const startDate = this.shiftStartsAt.getDay();
const firstDayOfWeek = this.timeframeItem.getDay();
const isFirstCell = startDate === firstDayOfWeek;
const left =
isFirstCell || this.shiftStartDateOutOfRange
? '0px'
: `${
(DAYS_IN_WEEK - this.daysUntilEndOfTimeFrame) * this.shiftTimeUnitWidth +
ASSIGNEE_SPACER
}px`;
const width = `${this.shiftTimeUnitWidth * this.shiftWidth}px`;
return {
left,
width,
};
},
shiftStartsAt() {
return new Date(this.shift.startsAt);
},
shiftEndsAt() {
return new Date(this.shift.endsAt);
},
shiftStartDateOutOfRange() {
return this.shiftStartsAt.getTime() < this.timeframeItem.getTime();
},
shiftShouldRender() {
if (this.timeFrameIndex !== 0) {
return (
new Date(this.shiftRangeOverlap.overlapStartDate) > this.timeframeItem &&
new Date(this.shiftRangeOverlap.overlapStartDate) < this.currentTimeframeEndsAt
);
}
return Boolean(this.shiftRangeOverlap.daysOverlap);
},
shiftRangeOverlap() {
try {
return getOverlappingDaysInPeriods(
{ start: this.timeframeItem, end: this.currentTimeframeEndsAt },
{ start: this.shiftStartsAt, end: this.shiftEndsAt },
);
} catch (error) {
// TODO: We need to decide the UX implications of a invalid date creation.
return { daysOverlap: 0 };
}
},
shiftWidth() {
const offset = this.shiftStartDateOutOfRange ? 0 : 1;
const baseWidth =
this.timeFrameIndex === 0
? this.totalShiftRangeOverlap.daysOverlap
: this.shiftRangeOverlap.daysOverlap;
return baseWidth + offset;
},
timeFrameIndex() {
return this.timeframe.indexOf(this.timeframeItem);
},
timeFrameEndsAt() {
return this.timeframe[this.timeframe.length - 1];
},
totalShiftRangeOverlap() {
return getOverlappingDaysInPeriods(
{
start: this.timeframeItem,
end: incrementDateByDays(this.timeFrameEndsAt, DAYS_IN_DATE_WEEK),
},
{ start: this.shiftStartsAt, end: this.shiftEndsAt },
);
},
},
};
</script>
<template>
<rotation-assignee
v-if="shiftShouldRender"
:assignee="shift.participant"
:rotation-assignee-style="rotationAssigneeStyle"
:rotation-assignee-starts-at="shift.startsAt"
:rotation-assignee-ends-at="shift.endsAt"
/>
</template>
...@@ -32,3 +32,18 @@ export const getTimeframeForWeeksView = (initialDate = new Date()) => { ...@@ -32,3 +32,18 @@ export const getTimeframeForWeeksView = (initialDate = new Date()) => {
return timeframe; return timeframe;
}; };
/**
* A utility function which extends a given date value by a certain amount of days.
*
* @param {Date} initial - the initial date to extend.
* @param {Number} increment - the amount of days to extend by.
* @returns {Date}
*
* @example
* incrementDateByDays(new Date(2021, 0, 10), 6) => new Date(2021, 0, 16)
*
*/
export const incrementDateByDays = (initial, increment) => {
return new Date(new Date().setDate(initial.getDate() + increment));
};
...@@ -24,3 +24,9 @@ export const PRESET_DEFAULTS = { ...@@ -24,3 +24,9 @@ export const PRESET_DEFAULTS = {
export const addRotationModalId = 'addRotationModal'; export const addRotationModalId = 'addRotationModal';
export const editRotationModalId = 'editRotationModal'; export const editRotationModalId = 'editRotationModal';
export const deleteRotationModalId = 'deleteRotationModal'; export const deleteRotationModalId = 'deleteRotationModal';
/**
* Used as a JavaScript week is represented as 0 - 6
*/
export const DAYS_IN_DATE_WEEK = 6;
export const ASSIGNEE_SPACER = 2;
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import produce from 'immer';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import getShiftTimeUnitWidthQuery from './graphql/queries/get_shift_time_unit_width.query.graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
const resolvers = {
Mutation: {
updateShiftTimeUnitWidth: (_, { shiftTimeUnitWidth = 0 }, { cache }) => {
const sourceData = cache.readQuery({ query: getShiftTimeUnitWidthQuery });
const data = produce(sourceData, (draftData) => {
// eslint-disable-next-line no-param-reassign
draftData.shiftTimeUnitWidth = shiftTimeUnitWidth;
});
cache.writeQuery({ query: getShiftTimeUnitWidthQuery, data });
},
},
};
export default new VueApollo({ export default new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(resolvers, {
{},
{
cacheConfig: {}, cacheConfig: {},
assumeImmutableResults: true, assumeImmutableResults: true,
}, }),
),
}); });
mutation updateShiftTimeUnitWidth($shiftTimeUnitWidth: Int) {
updateShiftTimeUnitWidth(shiftTimeUnitWidth: $shiftTimeUnitWidth) @client
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue'; import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue';
import getShiftTimeUnitWidthQuery from './graphql/queries/get_shift_time_unit_width.query.graphql';
import apolloProvider from './graphql'; import apolloProvider from './graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -12,6 +13,13 @@ export default () => { ...@@ -12,6 +13,13 @@ export default () => {
const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset; const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getShiftTimeUnitWidthQuery,
data: {
shiftTimeUnitWidth: 0,
},
});
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
......
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import { getDateInFuture } from '~/lib/utils/datetime_utility';
/** /**
* Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska * Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska
...@@ -18,19 +17,6 @@ export const getFormattedTimezone = (tz) => { ...@@ -18,19 +17,6 @@ export const getFormattedTimezone = (tz) => {
}); });
}; };
/**
* Returns formatted date of the rotation assignee
* based on the rotation start time and length
*
* @param {Date} startDate
* @param {Number} daysToAdd
*
* @returns {Date}
*/
export const assigneeScheduleDateStart = (startDate, daysToAdd) => {
return getDateInFuture(startDate, daysToAdd);
};
/** /**
* Returns `true` for non-empty string, otherwise returns `false` * Returns `true` for non-empty string, otherwise returns `false`
* *
......
...@@ -61,9 +61,6 @@ describe('AddScheduleModal', () => { ...@@ -61,9 +61,6 @@ describe('AddScheduleModal', () => {
} }
async function updateSchedule(localWrapper) { async function updateSchedule(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
localWrapper.find(GlModal).vm.$emit('primary', { preventDefault: jest.fn() }); localWrapper.find(GlModal).vm.$emit('primary', { preventDefault: jest.fn() });
} }
......
[{ [{
"id": "gid://gitlab/IncidentManagement::OncallRotation/2", "id": "gid://gitlab/IncidentManagement::OncallRotation/2",
"name": "Rotation 242", "name": "Rotation 242",
"startsAt": "2020-12-09T09:00:53Z", "startsAt": "2021-01-13T10:04:56.333Z",
"length": 1, "length": 1,
"lengthUnit": "WEEKS", "lengthUnit": "WEEKS",
"participants": { "participants": {
"nodes": [ "nodes": [
{ {
"user": { "user": {
"id": "gid://gitlab/User/1", "id": "gid://gitlab/IncidentManagement::OncallParticipant/49",
"username": "root", "username": "nora.schaden",
"avatarUrl": "/url" "avatarUrl": "/url"
}, },
"colorWeight": "500", "colorWeight": "500",
"colorPalette": "blue" "colorPalette": "blue"
}
]
}, },
"shifts": {
"nodes": [
{ {
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/49",
"colorWeight": "500",
"colorPalette": "blue",
"user": {
"username": "nora.schaden"
}
},
"startsAt": "2021-01-12T10:04:56.333Z",
"endsAt": "2021-01-15T10:04:56.333Z"
},
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/232",
"colorWeight": "500",
"colorPalette": "orange",
"user": { "user": {
"id": "gid://gitlab/User/2", "username": "racheal.loving"
"username": "david", }
},
"startsAt": "2021-01-16T10:04:56.333Z",
"endsAt": "2021-01-18T10:04:56.333Z"
}
]
}
},
{
"id": "gid://gitlab/IncidentManagement::OncallRotation/55",
"name": "Rotation 242",
"startsAt": "2021-01-13T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
"nodes": [
{
"user": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/99",
"username": "david.oregan",
"avatarUrl": "/url" "avatarUrl": "/url"
}, },
"colorWeight": "500", "colorWeight": "500",
"colorPalette": "magenta" "colorPalette": "aqua"
}
]
},
"shifts": {
"nodes": [
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/99",
"colorWeight": "500",
"colorPalette": "aqua",
"user": {
"username": "david.oregan"
}
},
"startsAt": "2021-01-14T10:04:56.333Z",
"endsAt": "2021-01-20T10:04:56.333Z"
},
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/300",
"colorWeight": "500",
"colorPalette": "green",
"user": {
"username": "david.keagan"
}
},
"startsAt": "2021-01-21T10:04:56.333Z",
"endsAt": "2021-01-26T10:04:56.333Z"
} }
] ]
} }
...@@ -30,29 +97,96 @@ ...@@ -30,29 +97,96 @@
{ {
"id": "gid://gitlab/IncidentManagement::OncallRotation/3", "id": "gid://gitlab/IncidentManagement::OncallRotation/3",
"name": "Rotation 244", "name": "Rotation 244",
"startsAt": "2020-12-16T09:00:53Z", "startsAt": "2021-01-06T10:04:56.333Z",
"length": 1, "length": 1,
"lengthUnit": "WEEKS", "lengthUnit": "WEEKS",
"participants": { "participants": {
"nodes": [ "nodes": [
{ {
"user": { "user": {
"id": "gid://gitlab/User/3", "id": "gid://gitlab/IncidentManagement::OncallParticipant/48",
"username": "root 2", "username": "root",
"avatarUrl": "/url" "avatarUrl": "/url"
}, },
"colorWeight": "500", "colorWeight": "500",
"colorPalette": "orange" "colorPalette": "magenta"
}
]
},
"shifts": {
"nodes": [
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/100",
"colorWeight": "500",
"colorPalette": "magenta",
"user": {
"username": "root"
}
},
"startsAt": "2021-01-10T10:04:56.333Z",
"endsAt": "2021-01-13T10:04:56.333Z"
},
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/109",
"colorWeight": "600",
"colorPalette": "blue",
"user": {
"username": "root2"
}
}, },
"startsAt": "2021-01-15T10:04:56.333Z",
"endsAt": "2021-01-18T10:04:56.333Z"
}
]
}
},
{
"id": "gid://gitlab/IncidentManagement::OncallRotation/5",
"name": "Rotation 247",
"startsAt": "2021-01-06T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
"nodes": [
{ {
"user": { "user": {
"id": "gid://gitlab/User/4", "id": "gid://gitlab/IncidentManagement::OncallParticipant/51",
"username": "david 2", "username": "oregand",
"avatarUrl": "/url" "avatarUrl": "/url"
}, },
"colorWeight": "500", "colorWeight": "600",
"colorPalette": "aqua" "colorPalette": "orange"
}
]
},
"shifts": {
"nodes": [
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/52",
"colorWeight": "600",
"colorPalette": "orange",
"user": {
"username": "oregand"
}
},
"startsAt": "2021-01-12T10:04:56.333Z",
"endsAt": "2021-01-15T10:04:56.333Z"
},
{
"participant": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/77",
"colorWeight": "600",
"colorPalette": "aqua",
"user": {
"username": "sarah.w"
}
},
"startsAt": "2021-01-16T10:04:56.333Z",
"endsAt": "2021-01-30T10:04:56.333Z"
} }
] ]
} }
}] }]
\ No newline at end of file
...@@ -11,7 +11,6 @@ import VueApollo from 'vue-apollo'; ...@@ -11,7 +11,6 @@ import VueApollo from 'vue-apollo';
import { preExistingSchedule, newlyCreatedSchedule } from './mocks/apollo_mock'; import { preExistingSchedule, newlyCreatedSchedule } from './mocks/apollo_mock';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo);
describe('On-call schedule wrapper', () => { describe('On-call schedule wrapper', () => {
let wrapper; let wrapper;
...@@ -45,6 +44,7 @@ describe('On-call schedule wrapper', () => { ...@@ -45,6 +44,7 @@ describe('On-call schedule wrapper', () => {
function mountComponentWithApollo() { function mountComponentWithApollo() {
const fakeApollo = createMockApollo([[getOncallSchedulesQuery, getOncallSchedulesQuerySpy]]); const fakeApollo = createMockApollo([[getOncallSchedulesQuery, getOncallSchedulesQuerySpy]]);
localVue.use(VueApollo);
wrapper = shallowMount(OnCallScheduleWrapper, { wrapper = shallowMount(OnCallScheduleWrapper, {
localVue, localVue,
...@@ -64,7 +64,6 @@ describe('On-call schedule wrapper', () => { ...@@ -64,7 +64,6 @@ describe('On-call schedule wrapper', () => {
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
} }
}); });
......
...@@ -2,26 +2,31 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,26 +2,31 @@ import { shallowMount } from '@vue/test-utils';
import { GlToken, GlAvatarLabeled, GlPopover } from '@gitlab/ui'; import { GlToken, GlAvatarLabeled, GlPopover } from '@gitlab/ui';
import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue'; import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { formatDate } from '~/lib/utils/datetime_utility';
import mockRotations from '../../mocks/mock_rotation.json'; import mockRotations from '../../mocks/mock_rotation.json';
describe('RotationAssignee', () => { describe('RotationAssignee', () => {
let wrapper; let wrapper;
const assignee = mockRotations[0].participants.nodes[1]; const assignee = mockRotations[0].shifts.nodes[0];
const findToken = () => wrapper.find(GlToken); const findToken = () => wrapper.findComponent(GlToken);
const findAvatar = () => wrapper.find(GlAvatarLabeled); const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
const findPopOver = () => wrapper.find(GlPopover); const findPopOver = () => wrapper.findComponent(GlPopover);
const findStartsAt = () => wrapper.findByTestId('rotation-assignee-starts-at'); const findStartsAt = () => wrapper.findByTestId('rotation-assignee-starts-at');
const findEndsAt = () => wrapper.findByTestId('rotation-assignee-ends-at'); const findEndsAt = () => wrapper.findByTestId('rotation-assignee-ends-at');
const formattedDate = (date) => {
return formatDate(date, 'mmmm d, yyyy, hh:mm');
};
function createComponent() { function createComponent() {
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(RotationAssignee, { shallowMount(RotationAssignee, {
propsData: { propsData: {
assignee, assignee: assignee.participant,
assigneeIndex: 1, rotationAssigneeStartsAt: assignee.startsAt,
rotationLength: mockRotations[0].length, rotationAssigneeEndsAt: assignee.endsAt,
rotationStartsAt: mockRotations[0].startsAt, rotationAssigneeStyle: { left: '0px', width: '100px' },
}, },
}), }),
); );
...@@ -37,26 +42,20 @@ describe('RotationAssignee', () => { ...@@ -37,26 +42,20 @@ describe('RotationAssignee', () => {
describe('rotation assignee token', () => { describe('rotation assignee token', () => {
it('should render an assignee name', () => { it('should render an assignee name', () => {
expect(findAvatar().attributes('label')).toBe(assignee.user.username); expect(findAvatar().attributes('label')).toBe(assignee.participant.user.username);
});
it('should render an assignee avatar', () => {
expect(findAvatar().attributes('src')).toBe(assignee.user.avatarUrl);
}); });
it('should render an assignee color based on the chevron skipping color pallette', () => { it('should render an assignee color based on the chevron skipping color pallette', () => {
const token = findToken(); const token = findToken();
expect(token.classes()).toContain( expect(token.classes()).toContain(
`gl-bg-data-viz-${assignee.colorPalette}-${assignee.colorWeight}`, `gl-bg-data-viz-${assignee.participant.colorPalette}-${assignee.participant.colorWeight}`,
); );
}); });
it('should render an assignee schedule and rotation information in a popover', () => { it('should render an assignee schedule and rotation information in a popover', () => {
expect(findPopOver().attributes('target')).toBe(assignee.user.id); expect(findPopOver().attributes('target')).toBe(assignee.participant.id);
// starts at the beginning of the rotation time expect(findStartsAt().text()).toContain(formattedDate(assignee.startsAt));
expect(findStartsAt().text()).toContain('12/16/2020'); expect(findEndsAt().text()).toContain(formattedDate(assignee.endsAt));
// ends at the calculated length of the rotation for this user: rotation length * which user index assignee is at
expect(findEndsAt().text()).toContain('12/23/2020');
}); });
}); });
}); });
...@@ -16,72 +16,217 @@ exports[`RotationsListSectionComponent renders component layout 1`] = ` ...@@ -16,72 +16,217 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
Rotation 242 Rotation 242
</span> </span>
<gl-button-group-stub <div
class="gl-px-2" class="gl-px-2 btn-group"
role="group"
> >
<gl-button-stub <button
aria-label="Edit rotation" aria-label="Edit rotation"
buttontextclasses="" class="btn btn-default btn-md gl-button btn-default-tertiary btn-icon"
category="tertiary"
icon="pencil"
role="button"
size="medium"
tabindex="0"
title="Edit rotation" title="Edit rotation"
variant="default" type="button"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="pencil-icon"
>
<use
href="#pencil"
/> />
</svg>
<gl-button-stub <!---->
</button>
<button
aria-label="Delete rotation" aria-label="Delete rotation"
buttontextclasses="" class="btn btn-default btn-md gl-button btn-default-tertiary btn-icon"
category="tertiary"
icon="remove"
role="button"
size="medium"
tabindex="0"
title="Delete rotation" title="Delete rotation"
variant="default" type="button"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="remove-icon"
>
<use
href="#remove"
/> />
</gl-button-group-stub> </svg>
<!---->
</button>
</div>
</span> </span>
<span <span
class="timeline-cell" class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
data-testid="timelineCell" data-testid="timelineCell"
> >
<current-day-indicator-stub <span
presettype="WEEKS" class="current-day-indicator"
timeframeitem="Mon Jan 01 2018 00:00:00 GMT+0000 (Greenwich Mean Time)" style="left: 7.142857142857143%;"
/> />
<rotation-assignee-stub <div
assignee="[object Object]" class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden"
assigneeindex="0" style="left: 0px; width: 0px;"
rotationlength="1" >
rotationstartsat="2020-12-09T09:00:53Z" <span
/> class="gl-w-full gl-h-6 gl-align-items-center gl-token gl-token-default-variant gl-bg-data-viz-blue-500"
id="gid://gitlab/IncidentManagement::OncallParticipant/49"
>
<span
class="gl-token-content"
>
<div
class="gl-avatar-labeled"
shape="circle"
size="16"
title="nora.schaden"
>
<div
class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s16 gl-avatar-identicon-bg1"
title="nora.schaden"
>
</div>
<div
class="gl-avatar-labeled-labels gl-text-left!"
>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-text-left! gl-mx-n1 gl-my-n1"
>
<span
class="gl-avatar-labeled-label"
>
nora.schaden
</span> </span>
</div>
<span <span
class="timeline-cell" class="gl-avatar-labeled-sublabel"
>
</span>
</div>
</div>
<!---->
</span>
</span>
<div
class="gl-popover"
title="nora.schaden"
>
<p
class="gl-m-0"
data-testid="rotation-assignee-starts-at"
>
Starts: January 12, 2021, 10:01
</p>
<p
class="gl-m-0"
data-testid="rotation-assignee-ends-at"
>
Ends: January 15, 2021, 10:01
</p>
</div>
</div>
<div
class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden"
style="left: 2px; width: 0px;"
>
<span
class="gl-w-full gl-h-6 gl-align-items-center gl-token gl-token-default-variant gl-bg-data-viz-orange-500"
id="gid://gitlab/IncidentManagement::OncallParticipant/232"
>
<span
class="gl-token-content"
>
<div
class="gl-avatar-labeled"
shape="circle"
size="16"
title="racheal.loving"
>
<div
class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s16 gl-avatar-identicon-bg1"
title="racheal.loving"
>
</div>
<div
class="gl-avatar-labeled-labels gl-text-left!"
>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-text-left! gl-mx-n1 gl-my-n1"
>
<span
class="gl-avatar-labeled-label"
>
racheal.loving
</span>
</div>
<span
class="gl-avatar-labeled-sublabel"
>
</span>
</div>
</div>
<!---->
</span>
</span>
<div
class="gl-popover"
title="racheal.loving"
>
<p
class="gl-m-0"
data-testid="rotation-assignee-starts-at"
>
Starts: January 16, 2021, 10:01
</p>
<p
class="gl-m-0"
data-testid="rotation-assignee-ends-at"
>
Ends: January 18, 2021, 10:01
</p>
</div>
</div>
</span>
<span
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-overflow-hidden"
data-testid="timelineCell" data-testid="timelineCell"
> >
<current-day-indicator-stub <!---->
presettype="WEEKS"
timeframeitem="Mon Jan 08 2018 00:00:00 GMT+0000 (Greenwich Mean Time)"
/>
<rotation-assignee-stub <!---->
assignee="[object Object]" <!---->
assigneeindex="1"
rotationlength="1"
rotationstartsat="2020-12-09T09:00:53Z"
/>
</span> </span>
</div> </div>
<delete-rotation-modal-stub <!---->
modalid="deleteRotationModal"
rotation="[object Object]"
/>
</div> </div>
`; `;
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import WeeksHeaderSubItemComponent from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue'; import WeeksHeaderSubItemComponent from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils'; import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import updateShiftTimeUnitWidthMutation from 'ee/oncall_schedules/graphql/mutations/update_shift_time_unit_width.mutation.graphql';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('WeeksHeaderSubItemComponent', () => { describe('WeeksHeaderSubItemComponent', () => {
let wrapper; let wrapper;
...@@ -11,11 +14,21 @@ describe('WeeksHeaderSubItemComponent', () => { ...@@ -11,11 +14,21 @@ describe('WeeksHeaderSubItemComponent', () => {
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
function mountComponent({ timeframeItem = mockTimeframeWeeks[0] }) { function mountComponent({ timeframeItem = mockTimeframeWeeks[0] }) {
wrapper = shallowMount(WeeksHeaderSubItemComponent, { wrapper = extendedWrapper(
shallowMount(WeeksHeaderSubItemComponent, {
propsData: { propsData: {
timeframeItem, timeframeItem,
}, },
}); directives: {
GlResizeObserver: createMockDirective(),
},
mocks: {
$apollo: {
mutate: jest.fn(),
},
},
}),
);
} }
beforeEach(() => { beforeEach(() => {
...@@ -25,11 +38,11 @@ describe('WeeksHeaderSubItemComponent', () => { ...@@ -25,11 +38,11 @@ describe('WeeksHeaderSubItemComponent', () => {
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
} }
}); });
const findSublabelValues = () => wrapper.findAll('[data-testid="sublabel-value"]'); const findSublabelValues = () => wrapper.findAll('[data-testid="sublabel-value"]');
const findWeeksHeaderSubItemComponent = () => wrapper.findByTestId('week-item-sublabel');
describe('computed', () => { describe('computed', () => {
describe('headerSubItems', () => { describe('headerSubItems', () => {
...@@ -71,5 +84,24 @@ describe('WeeksHeaderSubItemComponent', () => { ...@@ -71,5 +84,24 @@ describe('WeeksHeaderSubItemComponent', () => {
expect.arrayContaining(['label-dark', 'label-bold']), expect.arrayContaining(['label-dark', 'label-bold']),
); );
}); });
it('should store the rendered cell width in Apollo cache via `updateShiftTimeUnitWidthMutation` when mounted', async () => {
wrapper.vm.$apollo.mutate.mockResolvedValueOnce({});
await wrapper.vm.$nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateShiftTimeUnitWidthMutation,
variables: {
shiftTimeUnitWidth: wrapper.vm.$refs.weeklyDayCell[0].offsetWidth,
},
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
});
it('should re-calculate cell width inside Apollo cache on page resize', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
const { value } = getBinding(findWeeksHeaderSubItemComponent().element, 'gl-resize-observer');
value();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(2);
});
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlCard } from '@gitlab/ui'; import { GlCard } from '@gitlab/ui';
import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue'; import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue';
import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue'; import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue';
...@@ -9,19 +9,23 @@ import mockRotations from '../../mocks/mock_rotation.json'; ...@@ -9,19 +9,23 @@ import mockRotations from '../../mocks/mock_rotation.json';
describe('RotationsListSectionComponent', () => { describe('RotationsListSectionComponent', () => {
let wrapper; let wrapper;
const mockTimeframeInitialDate = new Date(2018, 0, 1); const mockTimeframeInitialDate = new Date(mockRotations[0].shifts.nodes[0].startsAt);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const projectPath = 'group/project';
function createComponent({ function createComponent({
presetType = PRESET_TYPES.WEEKS, presetType = PRESET_TYPES.WEEKS,
timeframe = mockTimeframeWeeks, timeframe = mockTimeframeWeeks,
} = {}) { } = {}) {
wrapper = shallowMount(RotationsListSection, { wrapper = mount(RotationsListSection, {
propsData: { propsData: {
presetType, presetType,
timeframe, timeframe,
rotations: [mockRotations[0]], rotations: [mockRotations[0]],
}, },
provide: {
projectPath,
},
stubs: { stubs: {
GlCard, GlCard,
}, },
...@@ -39,7 +43,7 @@ describe('RotationsListSectionComponent', () => { ...@@ -39,7 +43,7 @@ describe('RotationsListSectionComponent', () => {
}); });
const findTimelineCells = () => wrapper.findAll('[data-testid="timelineCell"]'); const findTimelineCells = () => wrapper.findAll('[data-testid="timelineCell"]');
const findRotationAssignees = () => wrapper.findAll(RotationsAssignee); const findRotationAssignees = () => wrapper.findAllComponents(RotationsAssignee);
it('renders component layout', () => { it('renders component layout', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
...@@ -53,10 +57,10 @@ describe('RotationsListSectionComponent', () => { ...@@ -53,10 +57,10 @@ describe('RotationsListSectionComponent', () => {
expect(findTimelineCells().at(0).find(CurrentDayIndicator).exists()).toBe(true); expect(findTimelineCells().at(0).find(CurrentDayIndicator).exists()).toBe(true);
}); });
it('render the correct amount of rotation assignees with their name, avatar and color', () => { it('render the correct amount of rotation assignees with their related information', () => {
expect(findRotationAssignees()).toHaveLength(2); expect(findRotationAssignees()).toHaveLength(2);
expect(findRotationAssignees().at(0).props().assignee.user).toEqual( expect(findRotationAssignees().at(0).props().assignee.user).toEqual(
mockRotations[0].participants.nodes[0].user, mockRotations[0].shifts.nodes[0].participant.user,
); );
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import ScheduleShift from 'ee/oncall_schedules/components/schedule/components/schedule_shift.vue';
import RotationsAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { incrementDateByDays } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
const shift = {
participant: {
id: '1',
user: {
username: 'nora.schaden',
},
},
startsAt: '2021-01-12T10:04:56.333Z',
endsAt: '2021-01-15T10:04:56.333Z',
};
const CELL_WIDTH = 50;
const timeframeItem = new Date(2021, 0, 13);
const timeframe = [timeframeItem, incrementDateByDays(timeframeItem, DAYS_IN_WEEK)];
describe('ee/oncall_schedules/components/schedule/components/schedule_shift.vue', () => {
let wrapper;
function createComponent({ props = {}, data = {} } = {}) {
wrapper = shallowMount(ScheduleShift, {
propsData: {
shift,
shiftIndex: 0,
timeframeItem,
timeframe,
presetType: PRESET_TYPES.WEEKS,
...props,
},
data() {
return {
shiftTimeUnitWidth: 0,
...data,
};
},
mocks: {
$apollo: {
queries: {
shiftTimeUnitWidth: 0,
},
},
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findRotationAssignee = () => wrapper.findComponent(RotationsAssignee);
describe('shift overlaps inside the current time-frame', () => {
it('should render a rotation assignee child component', () => {
expect(findRotationAssignee().exists()).toBe(true);
});
it('calculates the correct rotation assignee styles when the shift starts at the beginning of the time-frame cell', () => {
/**
* Where left should be 0px i.e. beginning of time-frame cell
* and width should be overlapping days * CELL_WIDTH(3 * 50)
*/
createComponent({ data: { shiftTimeUnitWidth: CELL_WIDTH } });
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '0px',
width: '150px',
});
});
it('calculates the correct rotation assignee styles when the shift does not start at the beginning of the time-frame cell', () => {
/**
* Where left should be 52px i.e. ((DAYS_IN_WEEK - (timeframeEndsAt - overlapStartDate)) * CELL_WIDTH) + ASSIGNEE_SPACER(((7 - (20 - 14)) * 50)) + 2
* and width should be overlapping days * (CELL_WIDTH + offset)(1 * (50 + 50))
* where offset is either CELL_WIDTH * 0 or CELL_WIDTH * 1 depending on the index of the timeframe
*/
createComponent({
props: { shift: { ...shift, startsAt: '2021-01-14T10:04:56.333Z' } },
data: { shiftTimeUnitWidth: CELL_WIDTH },
});
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '52px',
width: '100px',
});
});
});
describe('shift does not overlap inside the current time-frame or contains an invalid date', () => {
it.each`
reason | expectedTimeframeItem | startsAt | endsAt
${'timeframe is an invalid date'} | ${new Date(NaN)} | ${shift.startsAt} | ${shift.endsAt}
${'shift start date is an invalid date'} | ${timeframeItem} | ${'Invalid date string'} | ${shift.endsAt}
${'shift end date is an invalid date'} | ${timeframeItem} | ${shift.startsAt} | ${'Invalid date string'}
${'shift is not inside the timeframe'} | ${timeframeItem} | ${'2021-03-12T10:00:00.000Z'} | ${'2021-03-16T10:00:00.000Z'}
${'timeframe does not represent the shift times'} | ${new Date(2021, 3, 21)} | ${shift.startsAt} | ${shift.endsAt}
`(`should not render a rotation item when $reason`, (data) => {
const { expectedTimeframeItem, startsAt, endsAt } = data;
createComponent({
props: {
timeframeItem: expectedTimeframeItem,
shift: { ...shift, startsAt, endsAt },
},
});
expect(findRotationAssignee().exists()).toBe(false);
});
});
});
...@@ -10851,15 +10851,15 @@ msgstr "" ...@@ -10851,15 +10851,15 @@ msgstr ""
msgid "End Time" msgid "End Time"
msgstr "" msgstr ""
msgid "Ends at %{endsAt}"
msgstr ""
msgid "Ends at (UTC)" msgid "Ends at (UTC)"
msgstr "" msgstr ""
msgid "Ends on" msgid "Ends on"
msgstr "" msgstr ""
msgid "Ends: %{endsAt}"
msgstr ""
msgid "Enforce DNS rebinding attack protection" msgid "Enforce DNS rebinding attack protection"
msgstr "" msgstr ""
...@@ -15506,6 +15506,9 @@ msgstr "" ...@@ -15506,6 +15506,9 @@ msgstr ""
msgid "Invalid login or password" msgid "Invalid login or password"
msgstr "" msgstr ""
msgid "Invalid period"
msgstr ""
msgid "Invalid pin code" msgid "Invalid pin code"
msgstr "" msgstr ""
...@@ -26899,15 +26902,15 @@ msgstr "" ...@@ -26899,15 +26902,15 @@ msgstr ""
msgid "Starts %{startsIn}" msgid "Starts %{startsIn}"
msgstr "" msgstr ""
msgid "Starts at %{startsAt}"
msgstr ""
msgid "Starts at (UTC)" msgid "Starts at (UTC)"
msgstr "" msgstr ""
msgid "Starts on" msgid "Starts on"
msgstr "" msgstr ""
msgid "Starts: %{startsAt}"
msgstr ""
msgid "State your message to activate" msgid "State your message to activate"
msgstr "" msgstr ""
......
...@@ -842,3 +842,58 @@ describe('format24HourTimeStringFromInt', () => { ...@@ -842,3 +842,58 @@ describe('format24HourTimeStringFromInt', () => {
}); });
}); });
}); });
describe('getOverlappingDaysInPeriods', () => {
const start = new Date(2021, 0, 11);
const end = new Date(2021, 0, 13);
describe('when date periods overlap', () => {
const givenPeriodLeft = new Date(2021, 0, 11);
const givenPeriodRight = new Date(2021, 0, 14);
it('returns an overlap object that contains the amount of days overlapping, start date of overlap and end date of overlap', () => {
expect(
datetimeUtility.getOverlappingDaysInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
).toEqual({
daysOverlap: 2,
overlapStartDate: givenPeriodLeft.getTime(),
overlapEndDate: end.getTime(),
});
});
});
describe('when date periods do not overlap', () => {
const givenPeriodLeft = new Date(2021, 0, 9);
const givenPeriodRight = new Date(2021, 0, 10);
it('returns an overlap object that contains a 0 value for days overlapping', () => {
expect(
datetimeUtility.getOverlappingDaysInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
).toEqual({ daysOverlap: 0 });
});
});
describe('when date periods contain an invalid Date', () => {
const startInvalid = new Date(NaN);
const endInvalid = new Date(NaN);
const error = __('Invalid period');
it('throws an exception when the left period contains an invalid date', () => {
expect(() =>
datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start: startInvalid, end }),
).toThrow(error);
});
it('throws an exception when the right period contains an invalid date', () => {
expect(() =>
datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start, end: endInvalid }),
).toThrow(error);
});
});
});
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