Commit 6d4a29e9 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'oncall-schedule-timeline-3-code-consolodate' into 'master'

Oncall schedule consolidate day and week code

See merge request gitlab-org/gitlab!58322
parents 5dcc79e3 5ef2aa6c
...@@ -4,8 +4,6 @@ import { isString, mapValues, isNumber, reduce } from 'lodash'; ...@@ -4,8 +4,6 @@ import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js'; import * as timeago from 'timeago.js';
import { languageCode, s__, __, n__ } from '../../locale'; import { languageCode, s__, __, n__ } from '../../locale';
const MILLISECONDS_IN_HOUR = 60 * 60 * 1000;
const MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR;
const DAYS_IN_WEEK = 7; const DAYS_IN_WEEK = 7;
window.timeago = timeago; window.timeago = timeago;
...@@ -946,49 +944,6 @@ export const format24HourTimeStringFromInt = (time) => { ...@@ -946,49 +944,6 @@ export const format24HourTimeStringFromInt = (time) => {
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} { daysOverlap: number of days the overlap is present, hoursOverlap: number of hours 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
* getOverlapDateInPeriods(
* { 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, hoursOverlap: 48, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
*
*/
export const getOverlapDateInPeriods = (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 {
hoursOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_HOUR),
daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY),
overlapStartDate,
overlapEndDate,
};
};
/** /**
* A utility function that checks that the date is today * A utility function that checks that the date is today
* *
......
...@@ -7,8 +7,8 @@ import { __, sprintf } from '~/locale'; ...@@ -7,8 +7,8 @@ import { __, sprintf } from '~/locale';
export const SHIFT_WIDTHS = { export const SHIFT_WIDTHS = {
md: 100, md: 100,
sm: 50, sm: 75,
xs: 25, xs: 20,
}; };
const ROTATION_CENTER_CLASS = 'gl-display-flex gl-justify-content-center gl-align-items-center'; const ROTATION_CENTER_CLASS = 'gl-display-flex gl-justify-content-center gl-align-items-center';
...@@ -77,7 +77,7 @@ export default { ...@@ -77,7 +77,7 @@ export default {
</script> </script>
<template> <template>
<div class="gl-absolute gl-h-7 gl-mt-3" :style="rotationAssigneeStyle"> <div class="gl-absolute gl-h-7 gl-mt-3 gl-pr-1" :style="rotationAssigneeStyle">
<div <div
:id="rotationAssigneeUniqueID" :id="rotationAssigneeUniqueID"
class="gl-h-6" class="gl-h-6"
......
...@@ -13,6 +13,11 @@ export default { ...@@ -13,6 +13,11 @@ export default {
type: [Date, Object], type: [Date, Object],
required: true, required: true,
}, },
timelineWidth: {
type: Number,
required: false,
default: 1,
},
}, },
computed: { computed: {
isVisible() { isVisible() {
...@@ -32,7 +37,7 @@ export default { ...@@ -32,7 +37,7 @@ export default {
<template> <template>
<span <span
v-if="isVisible" v-if="isVisible"
:style="getIndicatorStyles(presetType, timeframeItem)" :style="getIndicatorStyles(presetType, timeframeItem, timelineWidth)"
data-testid="current-day-indicator" data-testid="current-day-indicator"
class="current-day-indicator" class="current-day-indicator"
></span> ></span>
......
<script> <script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { PRESET_TYPES, HOURS_IN_DAY } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES, HOURS_IN_DAY } from 'ee/oncall_schedules/constants';
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 CommonMixin from 'ee/oncall_schedules/mixins/common_mixin';
export default { export default {
PRESET_TYPES, PRESET_TYPES,
HOURS_IN_DAY, HOURS_IN_DAY,
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [CommonMixin], mixins: [CommonMixin],
props: { props: {
timeframeItem: { timeframeItem: {
...@@ -17,28 +12,11 @@ export default { ...@@ -17,28 +12,11 @@ export default {
required: true, required: true,
}, },
}, },
mounted() {
this.updateShiftStyles();
},
methods: {
updateShiftStyles() {
this.$apollo.mutate({
mutation: updateShiftTimeUnitWidthMutation,
variables: {
shiftTimeUnitWidth: this.$refs.dailyHourCell[0].offsetWidth,
},
});
},
},
}; };
</script> </script>
<template> <template>
<div <div class="item-sublabel" data-testid="day-item-sublabel">
v-gl-resize-observer="updateShiftStyles"
class="item-sublabel"
data-testid="day-item-sublabel"
>
<span <span
v-for="hour in $options.HOURS_IN_DAY" v-for="hour in $options.HOURS_IN_DAY"
:key="hour" :key="hour"
......
<script> <script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { PRESET_TYPES } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
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 CommonMixin from 'ee/oncall_schedules/mixins/common_mixin';
export default { export default {
PRESET_TYPES, PRESET_TYPES,
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [CommonMixin], mixins: [CommonMixin],
props: { props: {
timeframeItem: { timeframeItem: {
...@@ -33,9 +28,6 @@ export default { ...@@ -33,9 +28,6 @@ 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
...@@ -49,24 +41,12 @@ export default { ...@@ -49,24 +41,12 @@ export default {
getSubItemValue(subItem) { getSubItemValue(subItem) {
return subItem.getDate(); return subItem.getDate();
}, },
updateShiftStyles() {
this.$apollo.mutate({
mutation: updateShiftTimeUnitWidthMutation,
variables: {
shiftTimeUnitWidth: this.$refs.weeklyDayCell[0].offsetWidth,
},
});
},
}, },
}; };
</script> </script>
<template> <template>
<div <div class="item-sublabel" data-testid="week-item-sublabel">
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"
......
...@@ -11,7 +11,6 @@ import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/compon ...@@ -11,7 +11,6 @@ import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/compon
import { import {
editRotationModalId, editRotationModalId,
deleteRotationModalId, deleteRotationModalId,
PRESET_TYPES,
TIMELINE_CELL_WIDTH, TIMELINE_CELL_WIDTH,
} from 'ee/oncall_schedules/constants'; } from 'ee/oncall_schedules/constants';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -68,21 +67,9 @@ export default { ...@@ -68,21 +67,9 @@ export default {
}; };
}, },
computed: { computed: {
presetIsDay() {
return this.presetType === PRESET_TYPES.DAYS;
},
timeframeToDraw() {
if (this.presetIsDay) {
return [this.timeframe[0]];
}
return this.timeframe;
},
timelineStyles() { timelineStyles() {
const length = this.presetIsDay ? 1 : 2;
return { return {
width: `calc((${100}% - ${TIMELINE_CELL_WIDTH}px) / ${length})`, width: `calc(${100}% - ${TIMELINE_CELL_WIDTH}px)`,
}; };
}, },
}, },
...@@ -91,12 +78,6 @@ export default { ...@@ -91,12 +78,6 @@ export default {
this.rotationToUpdate = rotation; this.rotationToUpdate = rotation;
this.$emit('set-rotation-to-update', rotation); this.$emit('set-rotation-to-update', rotation);
}, },
cellShouldHideOverflow(index) {
return index + 1 === this.timeframe.length || this.presetIsDay;
},
timeframeItemUniqueKey(timeframeItem) {
return timeframeItem.valueOf();
},
}, },
}; };
</script> </script>
...@@ -111,13 +92,15 @@ export default { ...@@ -111,13 +92,15 @@ export default {
<span class="gl-text-truncate">{{ $options.i18n.addRotationLabel }}</span> <span class="gl-text-truncate">{{ $options.i18n.addRotationLabel }}</span>
</span> </span>
<span <span
v-for="(timeframeItem, index) in timeframeToDraw" class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-overflow-hidden"
:key="index"
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:style="timelineStyles" :style="timelineStyles"
data-testid="empty-timeline-cell" data-testid="empty-timeline-cell"
> >
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" /> <current-day-indicator
:preset-type="presetType"
:timeframe-item="timeframe[0]"
:timeline-width="2"
/>
</span> </span>
</div> </div>
<div v-else> <div v-else>
...@@ -154,18 +137,18 @@ export default { ...@@ -154,18 +137,18 @@ export default {
</gl-button-group> </gl-button-group>
</span> </span>
<span <span
v-for="(timeframeItem, index) in timeframeToDraw" class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-overflow-hidden"
:key="timeframeItemUniqueKey(timeframeItem)"
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:class="{ 'gl-overflow-hidden': cellShouldHideOverflow(index) }"
:style="timelineStyles" :style="timelineStyles"
data-testid="timeline-cell" data-testid="timeline-cell"
> >
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" /> <current-day-indicator
:preset-type="presetType"
:timeframe-item="timeframe[0]"
:timeline-width="2"
/>
<schedule-shift-wrapper <schedule-shift-wrapper
v-if="rotation.shifts" v-if="rotation.shifts"
:preset-type="presetType" :preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe" :timeframe="timeframe"
:rotation="rotation" :rotation="rotation"
/> />
......
<script>
import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { HOURS_IN_DAY, ASSIGNEE_SPACER } from 'ee/oncall_schedules/constants';
import { getOverlapDateInPeriods } from '~/lib/utils/datetime_utility';
import { currentTimeframeEndsAt } from './shift_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,
},
shiftTimeUnitWidth: {
type: Number,
required: true,
},
},
computed: {
currentTimeframeEndsAt() {
return currentTimeframeEndsAt(this.timeframeItem, this.presetType);
},
hoursUntilEndOfTimeFrame() {
return HOURS_IN_DAY - new Date(this.shiftRangeOverlap.overlapStartDate).getHours();
},
rotationAssigneeStyle() {
return {
left: `${this.shiftLeft}px`,
width: `${this.shiftWidth}px`,
};
},
shiftEndsAt() {
return new Date(this.shift.endsAt);
},
shiftLeft() {
const shouldStartAtBeginningOfCell =
this.shiftStartsAt.getHours() === 0 || this.shiftStartHourOutOfRange;
return shouldStartAtBeginningOfCell
? 0
: (HOURS_IN_DAY - this.hoursUntilEndOfTimeFrame) * this.shiftTimeUnitWidth;
},
shiftRangeOverlap() {
try {
return getOverlapDateInPeriods(
{ start: this.timeframeItem, end: this.currentTimeframeEndsAt },
{ start: this.shiftStartsAt, end: this.shiftEndsAt },
);
} catch (error) {
return { hoursOverlap: 0 };
}
},
shiftStartsAt() {
return new Date(this.shift.startsAt);
},
shiftStartHourOutOfRange() {
return this.shiftStartsAt.getTime() < this.timeframeItem.getTime();
},
shiftWidth() {
const baseWidth =
this.shiftEndsAt.getTime() >= this.currentTimeframeEndsAt.getTime()
? HOURS_IN_DAY
: this.shiftRangeOverlap.hoursOverlap + this.shiftOffset;
return this.shiftTimeUnitWidth * baseWidth - ASSIGNEE_SPACER;
},
shiftOffset() {
return (this.shiftStartsAt.getTimezoneOffset() - this.shiftEndsAt.getTimezoneOffset()) / 60;
},
},
};
</script>
<template>
<rotation-assignee
:assignee="shift.participant"
:rotation-assignee-style="rotationAssigneeStyle"
:rotation-assignee-starts-at="shift.startsAt"
:rotation-assignee-ends-at="shift.endsAt"
:shift-width="shiftWidth"
/>
</template>
<script> <script>
import { PRESET_TYPES, SHIFT_WIDTH_CALCULATION_DELAY } from 'ee/oncall_schedules/constants'; import { SHIFT_WIDTH_CALCULATION_DELAY } from 'ee/oncall_schedules/constants';
import getShiftTimeUnitWidthQuery from 'ee/oncall_schedules/graphql/queries/get_shift_time_unit_width.query.graphql';
import getTimelineWidthQuery from 'ee/oncall_schedules/graphql/queries/get_timeline_width.query.graphql'; import getTimelineWidthQuery from 'ee/oncall_schedules/graphql/queries/get_timeline_width.query.graphql';
import DaysScheduleShift from './days_schedule_shift.vue'; import ShiftItem from './shift_item.vue';
import { shiftsToRender } from './shift_utils';
import WeeksScheduleShift from './weeks_schedule_shift.vue';
export default { export default {
components: { components: {
DaysScheduleShift, ShiftItem,
WeeksScheduleShift,
}, },
props: { props: {
presetType: { presetType: {
...@@ -20,10 +16,6 @@ export default { ...@@ -20,10 +16,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
timeframeItem: {
type: [Date, Object],
required: true,
},
timeframe: { timeframe: {
type: Array, type: Array,
required: true, required: true,
...@@ -31,41 +23,18 @@ export default { ...@@ -31,41 +23,18 @@ export default {
}, },
data() { data() {
return { return {
shiftTimeUnitWidth: 0,
componentByPreset: {
[PRESET_TYPES.DAYS]: DaysScheduleShift,
[PRESET_TYPES.WEEKS]: WeeksScheduleShift,
},
timelineWidth: 0, timelineWidth: 0,
}; };
}, },
apollo: { apollo: {
shiftTimeUnitWidth: {
query: getShiftTimeUnitWidthQuery,
debounce: SHIFT_WIDTH_CALCULATION_DELAY,
},
timelineWidth: { timelineWidth: {
query: getTimelineWidthQuery, query: getTimelineWidthQuery,
debounce: SHIFT_WIDTH_CALCULATION_DELAY, debounce: SHIFT_WIDTH_CALCULATION_DELAY,
}, },
}, },
computed: { computed: {
rotationLength() {
const { length, lengthUnit } = this.rotation;
return { length, lengthUnit };
},
shiftsToRender() { shiftsToRender() {
return Object.freeze( return Object.freeze(this.rotation.shifts.nodes);
shiftsToRender(
this.rotation.shifts.nodes,
this.timeframeItem,
this.presetType,
this.timeframeIndex,
),
);
},
timeframeIndex() {
return this.timeframe.indexOf(this.timeframeItem);
}, },
}, },
}; };
...@@ -73,17 +42,12 @@ export default { ...@@ -73,17 +42,12 @@ export default {
<template> <template>
<div> <div>
<component <shift-item
:is="componentByPreset[presetType]" v-for="shift in shiftsToRender"
v-for="(shift, shiftIndex) in shiftsToRender"
:key="shift.startAt" :key="shift.startAt"
:shift="shift" :shift="shift"
:shift-index="shiftIndex"
:preset-type="presetType" :preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe" :timeframe="timeframe"
:shift-time-unit-width="shiftTimeUnitWidth"
:rotation-length="rotationLength"
:timeline-width="timelineWidth" :timeline-width="timelineWidth"
/> />
</div> </div>
......
<script> <script>
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 { DAYS_IN_WEEK, HOURS_IN_DAY } from 'ee/oncall_schedules/constants'; import { getPixelOffset, getPixelWidth } from './shift_utils';
import { getOverlapDateInPeriods, nDaysAfter } from '~/lib/utils/datetime_utility';
import { weekDisplayShiftLeft, getPixelWidth } from './shift_utils';
export default { export default {
components: { components: {
...@@ -13,14 +11,6 @@ export default { ...@@ -13,14 +11,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
shiftIndex: {
type: Number,
required: true,
},
timeframeItem: {
type: [Date, Object],
required: true,
},
timeframe: { timeframe: {
type: Array, type: Array,
required: true, required: true,
...@@ -29,46 +19,22 @@ export default { ...@@ -29,46 +19,22 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
shiftTimeUnitWidth: {
type: Number,
required: true,
},
rotationLength: {
type: Object,
required: true,
},
timelineWidth: { timelineWidth: {
type: Number, type: Number,
required: true, required: true,
}, },
}, },
computed: { computed: {
currentTimeFrameEnd() {
return nDaysAfter(this.timeframeEndsAt, DAYS_IN_WEEK);
},
shiftStyles() { shiftStyles() {
const { const { timeframe, presetType, timelineWidth, shift } = this;
shiftUnitIsHour,
totalShiftRangeOverlap,
shiftStartDateOutOfRange,
shiftTimeUnitWidth,
shiftStartsAt,
timeframeItem,
presetType,
timelineWidth,
shift,
} = this;
return { return {
left: weekDisplayShiftLeft( left: getPixelOffset({
shiftUnitIsHour, timeframe,
totalShiftRangeOverlap, shift,
shiftStartDateOutOfRange, timelineWidth,
shiftTimeUnitWidth,
shiftStartsAt,
timeframeItem,
presetType, presetType,
), }),
width: Math.round( width: Math.round(
getPixelWidth({ getPixelWidth({
shift, shift,
...@@ -88,37 +54,6 @@ export default { ...@@ -88,37 +54,6 @@ export default {
width: `${width}px`, width: `${width}px`,
}; };
}, },
shiftStartsAt() {
return new Date(this.shift.startsAt);
},
shiftEndsAt() {
return new Date(this.shift.endsAt);
},
shiftStartDateOutOfRange() {
return this.shiftStartsAt.getTime() < this.timeframeItem.getTime();
},
shiftUnitIsHour() {
return (
this.totalShiftRangeOverlap.hoursOverlap <= HOURS_IN_DAY &&
this.rotationLength?.lengthUnit === 'HOURS'
);
},
timeframeEndsAt() {
return this.timeframe[this.timeframe.length - 1];
},
totalShiftRangeOverlap() {
try {
return getOverlapDateInPeriods(
{
start: this.timeframeItem,
end: this.currentTimeFrameEnd,
},
{ start: this.shiftStartsAt, end: this.shiftEndsAt },
);
} catch (error) {
return { hoursOverlap: 0 };
}
},
}, },
}; };
</script> </script>
......
import { import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
PRESET_TYPES,
DAYS_IN_WEEK,
ASSIGNEE_SPACER,
ASSIGNEE_SPACER_SMALL,
HOURS_IN_DAY,
} from 'ee/oncall_schedules/constants';
import {
getOverlapDateInPeriods,
getDayDifference,
nDaysAfter,
} from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
/**
* This method returns a Date that is
* n days after the start Date provided. This
* is used to calculate the end Date of a time
* frame item.
*
*
* @param {Date} timeframeStart - the current timeframe start Date.
* @param {String} presetType - the current grid type i.e. Week, Day, Hour.
* @returns {Date}
* @throws {Error} Uncaught Error: Invalid date
*
* @example
* currentTimeframeEndsAt(new Date(2021, 01, 07), 'WEEKS') => new Date(2021, 01, 14)
* currentTimeframeEndsAt(new Date(2021, 01, 07), 'DAYS') => new Date(2021, 01, 08)
*
*/
export const currentTimeframeEndsAt = (timeframeStart, presetType) => {
if (!(timeframeStart instanceof Date)) {
throw new Error(__('Invalid date'));
}
return nDaysAfter(timeframeStart, presetType === PRESET_TYPES.DAYS ? 1 : DAYS_IN_WEEK);
};
/**
* This method returns a Boolean
* to decide if a current shift item
* is valid for render by checking if there
* is an hoursOverlap greater than 0
*
*
* @param {Object} shiftRangeOverlap - current shift range overlap object.
* @returns {Boolean}
*
* @example
* shiftShouldRender({ hoursOverlap: 48 })
* => true
*
*/
export const shiftShouldRender = (shiftRangeOverlap) => {
return Boolean(shiftRangeOverlap.hoursOverlap);
};
/**
* This method extends shiftShouldRender for a week item
* by adding a conditional check for if the
* shift occurs after the first timeframe
* item, we need to check if the current shift
* starts on the timeframe start Date
*
*
* @param {Object} shiftRangeOverlap - current shift range overlap object.
* @param {Number} timeframeIndex - current timeframe index.
* @param {Date} shiftStartsAt - current shift start Date.
* @param {Date} timeframeItem - the current timeframe start Date.
* @returns {Boolean}
*
* @example
* weekShiftShouldRender({ overlapStartDate: 1610074800000, hoursOverlap: 3 }, 0, new Date(2021-01-07), new Date(2021-01-08))
* => true
*
*/
export const weekShiftShouldRender = (
shiftRangeOverlap,
timeframeIndex,
shiftStartsAt,
timeframeItem,
) => {
if (timeframeIndex === 0) {
return shiftShouldRender(shiftRangeOverlap);
}
return (
(shiftStartsAt >= timeframeItem ||
new Date(shiftRangeOverlap.overlapStartDate) > timeframeItem) &&
new Date(shiftRangeOverlap.overlapStartDate) <
currentTimeframeEndsAt(timeframeItem, PRESET_TYPES.WEEKS)
);
};
/**
* This method returns array of shifts to render
* against a current timeframe Date i.e.
* return any shifts that have an overlap with the current
* timeframe Date
*
*
* @param {Array} shifts - current array of shifts for a given rotation timeframe.
* @param {Date} timeframeItem - the current timeframe start Date.
* @param {String} presetType - the current grid type i.e. Week, Day, Hour.
* @param {Number} timeframeIndex - the index of the current timeframe.
* @returns {Array}
*
* @example
* shiftsToRender([{ startsAt: '2021-01-07', endsAt: '2021-01-08' }, { startsAt: '2021-01-016', endsAt: '2021-01-19' }], new Date(2021, 01, 07), 'WEEKS')
* => [{ startsAt: '2021-01-07', endsAt: '2021-01-08' }]
*
*/
export const shiftsToRender = (shifts, timeframeItem, presetType, timeframeIndex) => {
try {
const timeframeEndsAt = currentTimeframeEndsAt(timeframeItem, presetType);
const overlap = (startsAt, endsAt) =>
getOverlapDateInPeriods(
{ start: timeframeItem, end: timeframeEndsAt },
{ start: startsAt, end: endsAt },
);
if (presetType === PRESET_TYPES.DAYS) {
return shifts.filter(({ startsAt, endsAt }) => overlap(startsAt, endsAt).hoursOverlap > 0);
}
return shifts.filter(({ startsAt, endsAt }) =>
weekShiftShouldRender(
overlap(startsAt, endsAt),
timeframeIndex,
new Date(startsAt),
timeframeItem,
),
);
} catch (error) {
return [];
}
};
/**
* This method calculates the amount of days until the end of the current
* timeframe from where the current shift overlap begins at, taking
* into account when a timeframe might transition month during render
*
*
* @param {Object} shiftRangeOverlap - current shift range overlap object.
* @param {Date} timeframeItem - the current timeframe start Date.
* @param {String} presetType - the current grid type i.e. Week, Day, Hour.
* @returns {Number}
*
* @example
* daysUntilEndOfTimeFrame({ overlapStartDate: 1612814725387 }, Date Mon Feb 08 2021 15:04:57, 'WEEKS')
* => 7
* Where overlapStartDate is the timestamp equal to Date Mon Feb 08 2021 15:04:57
*
*/
export const daysUntilEndOfTimeFrame = (shiftRangeOverlap, timeframeItem, presetType) => {
const timeframeEndsAt = currentTimeframeEndsAt(timeframeItem, presetType);
const startDate = new Date(shiftRangeOverlap?.overlapStartDate);
if (timeframeEndsAt.getMonth() !== startDate.getMonth()) {
return Math.abs(getDayDifference(timeframeEndsAt, startDate));
}
return timeframeEndsAt.getDate() - startDate.getDate();
};
/**
* This method calculates the total left position of a current week
* rotation cell for less than 24 hours, equal to 24 hours
* or more than 24 hours
*
*
* @param {Boolean} shiftUnitIsHour - true if the current shift length is less than 24 hours.
* @param {Object} shiftRangeOverlap - current shift range overlap object.
* @param {Boolean} shiftStartDateOutOfRange - true if the current shift start date is outside of the current grid range.
* @param {String} shiftTimeUnitWidth - the current grid type i.e. Week, Day, Hour.
* @param {Date} shiftStartsAt - current shift start Date.
* @param {Date} timeframeItem - the current timeframe start Date.
* @param {String} presetType - the current grid type i.e. Week, Day, Hour.
* @returns {Number}
*
* @example
* weekDisplayShiftLeft(false, { daysOverlap: 3 }, false , 50, Date Mon Feb 08 2021 15:04:57, Date Mon Feb 08 2021 15:04:57, 'WEEKS')
* => 148
*
*/
export const weekDisplayShiftLeft = (
shiftUnitIsHour,
shiftRangeOverlap,
shiftStartDateOutOfRange,
shiftTimeUnitWidth,
shiftStartsAt,
timeframeItem,
presetType,
) => {
const startDate = shiftStartsAt.getDate();
const firstDayOfWeek = timeframeItem.getDate();
const shiftStartsEarly = startDate === firstDayOfWeek || shiftStartDateOutOfRange;
const daysUntilEnd = daysUntilEndOfTimeFrame(shiftRangeOverlap, timeframeItem, presetType);
const dayOffSet = (DAYS_IN_WEEK - daysUntilEnd) * shiftTimeUnitWidth;
if (shiftUnitIsHour) {
const hourOffset =
(shiftTimeUnitWidth / HOURS_IN_DAY) * new Date(shiftRangeOverlap.overlapStartDate).getHours();
return dayOffSet + Math.floor(hourOffset);
}
if (shiftStartsEarly) {
return 0;
}
return dayOffSet;
};
/**
* This method calculates the total width of a current week
* rotation cell for less than 24 hours, equal to 24 hours
* or more than 24 hours
*
*
* @param {Boolean} shiftUnitIsHour - true if the current shift length is less than 24 hours.
* @param {Object} shiftRangeOverlap - current shift range overlap object.
* @param {Boolean} shiftStartDateOutOfRange - true if the current shift start date is outside of the current grid range.
* @param {String} shiftTimeUnitWidth - the current grid type i.e. Week, Day, Hour.
* @returns {Number}
*
* @example
* weekDisplayShiftWidth(false, { daysOverlap: 3, hoursOverlap: 72, overlapEndDate: 1610496000000 }, false , 50)
* => 148
*
*/
export const weekDisplayShiftWidth = (
shiftUnitIsHour,
shiftRangeOverlap,
shiftStartDateOutOfRange,
shiftTimeUnitWidth,
) => {
if (shiftUnitIsHour) {
const SPACER = shiftRangeOverlap.hoursOverlap === 1 ? ASSIGNEE_SPACER_SMALL : ASSIGNEE_SPACER;
return (
Math.floor((shiftTimeUnitWidth / HOURS_IN_DAY) * shiftRangeOverlap.hoursOverlap) - SPACER
);
}
const shiftEndsAtMidnight = new Date(shiftRangeOverlap.overlapEndDate).getHours() === 0;
const widthOffset = shiftStartDateOutOfRange && !shiftEndsAtMidnight ? 1 : 0;
return shiftTimeUnitWidth * (shiftRangeOverlap.daysOverlap - widthOffset) - ASSIGNEE_SPACER;
};
// New utils, unused for now. Added as part of the // New utils, unused for now. Added as part of the
// https://gitlab.com/gitlab-org/gitlab/-/issues/324608 merge train. // https://gitlab.com/gitlab-org/gitlab/-/issues/324608 merge train.
......
...@@ -2,20 +2,12 @@ import produce from 'immer'; ...@@ -2,20 +2,12 @@ import produce from 'immer';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import getShiftTimeUnitWidthQuery from './graphql/queries/get_shift_time_unit_width.query.graphql';
import getTimelineWidthQuery from './graphql/queries/get_timeline_width.query.graphql'; import getTimelineWidthQuery from './graphql/queries/get_timeline_width.query.graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
const resolvers = { const resolvers = {
Mutation: { Mutation: {
updateShiftTimeUnitWidth: (_, { shiftTimeUnitWidth = 0 }, { cache }) => {
const sourceData = cache.readQuery({ query: getShiftTimeUnitWidthQuery });
const data = produce(sourceData, (draftData) => {
draftData.shiftTimeUnitWidth = shiftTimeUnitWidth;
});
cache.writeQuery({ query: getShiftTimeUnitWidthQuery, data });
},
updateTimelineWidth: (_, { timelineWidth = 0 }, { cache }) => { updateTimelineWidth: (_, { timelineWidth = 0 }, { cache }) => {
const sourceData = cache.readQuery({ query: getTimelineWidthQuery }); const sourceData = cache.readQuery({ query: getTimelineWidthQuery });
const data = produce(sourceData, (draftData) => { const data = produce(sourceData, (draftData) => {
......
mutation updateShiftTimeUnitWidth($shiftTimeUnitWidth: Int) {
updateShiftTimeUnitWidth(shiftTimeUnitWidth: $shiftTimeUnitWidth) @client
}
...@@ -2,7 +2,6 @@ import Vue from 'vue'; ...@@ -2,7 +2,6 @@ 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 apolloProvider from './graphql'; import apolloProvider from './graphql';
import getShiftTimeUnitWidthQuery from './graphql/queries/get_shift_time_unit_width.query.graphql';
import getTimelineWidthQuery from './graphql/queries/get_timeline_width.query.graphql'; import getTimelineWidthQuery from './graphql/queries/get_timeline_width.query.graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -14,13 +13,6 @@ export default () => { ...@@ -14,13 +13,6 @@ export default () => {
const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset; const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getShiftTimeUnitWidthQuery,
data: {
shiftTimeUnitWidth: 0,
},
});
apolloProvider.clients.defaultClient.cache.writeQuery({ apolloProvider.clients.defaultClient.cache.writeQuery({
query: getTimelineWidthQuery, query: getTimelineWidthQuery,
data: { data: {
......
...@@ -38,12 +38,16 @@ export default { ...@@ -38,12 +38,16 @@ export default {
this.$options.currentDate = currentDate; this.$options.currentDate = currentDate;
}, },
methods: { methods: {
getIndicatorStyles(presetType = PRESET_TYPES.WEEKS, timeframeStartDate = new Date()) { getIndicatorStyles(
presetType = PRESET_TYPES.WEEKS,
timeframeStartDate = new Date(),
timelineWidth = 1,
) {
if (presetType === PRESET_TYPES.DAYS) { if (presetType === PRESET_TYPES.DAYS) {
return this.getDayViewIndicatorStyles(); return this.getDayViewIndicatorStyles();
} }
return this.getWeekViewIndicatorStyles(timeframeStartDate); return this.getWeekViewIndicatorStyles(timeframeStartDate, timelineWidth);
}, },
getDayViewIndicatorStyles() { getDayViewIndicatorStyles() {
const currentDate = new Date(); const currentDate = new Date();
...@@ -54,14 +58,14 @@ export default { ...@@ -54,14 +58,14 @@ export default {
left: `${hours + minutes}%`, left: `${hours + minutes}%`,
}; };
}, },
getWeekViewIndicatorStyles(timeframeStartDate) { getWeekViewIndicatorStyles(timeframeStartDate, timelineWidth) {
const currentDate = new Date(); const currentDate = new Date();
const hourOffset = oneHourOffsetWeekView * currentDate.getHours(); const hourOffset = oneHourOffsetWeekView * currentDate.getHours();
const daysSinceShiftStart = getDayDifference(timeframeStartDate, currentDate); const daysSinceShiftStart = getDayDifference(timeframeStartDate, currentDate);
const leftOffset = oneDayOffsetWeekView * daysSinceShiftStart + hourOffset; const leftOffset = oneDayOffsetWeekView * daysSinceShiftStart + hourOffset;
return { return {
left: `${Math.round(leftOffset)}%`, left: `${leftOffset / timelineWidth}%`,
}; };
}, },
}, },
......
...@@ -68,18 +68,19 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders ...@@ -68,18 +68,19 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders
</span> </span>
<span <span
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1" class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-overflow-hidden"
data-testid="timeline-cell" data-testid="timeline-cell"
style="width: calc(100% - 180px);"
> >
<span <span
class="current-day-indicator" class="current-day-indicator"
data-testid="current-day-indicator" data-testid="current-day-indicator"
style="left: 29%;" style="left: 14.285714285714286%;"
/> />
<div> <div>
<div <div
class="gl-absolute gl-h-7 gl-mt-3" class="gl-absolute gl-h-7 gl-mt-3 gl-pr-1"
style="left: 0px; width: 0px;" style="left: 0px; width: 0px;"
> >
<div <div
...@@ -119,7 +120,7 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders ...@@ -119,7 +120,7 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders
</div> </div>
</div> </div>
<div <div
class="gl-absolute gl-h-7 gl-mt-3" class="gl-absolute gl-h-7 gl-mt-3 gl-pr-1"
style="left: 0px; width: 0px;" style="left: 0px; width: 0px;"
> >
<div <div
...@@ -160,14 +161,6 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders ...@@ -160,14 +161,6 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders
</div> </div>
</div> </div>
</span> </span>
<span
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-overflow-hidden"
data-testid="timeline-cell"
>
<!---->
<div />
</span>
</div> </div>
</div> </div>
......
...@@ -43,9 +43,10 @@ describe('CurrentDayIndicator', () => { ...@@ -43,9 +43,10 @@ describe('CurrentDayIndicator', () => {
* and the current day is the following Wednesday. * and the current day is the following Wednesday.
* This creates a gap of two days so our generated offset should represent: * This creates a gap of two days so our generated offset should represent:
* DayDiffOffset + weeklyOffset + weeklyHourOffset * DayDiffOffset + weeklyOffset + weeklyHourOffset
* 29 + 0 * Note: We do not round these calculations
* 28.571428571428573 + 0
*/ */
const leftOffset = '29'; const leftOffset = '28.571428571428573';
expect(wrapper.attributes('style')).toBe(`left: ${leftOffset}%;`); expect(wrapper.attributes('style')).toBe(`left: ${leftOffset}%;`);
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DaysHeaderSubItem from 'ee/oncall_schedules/components/schedule/components/preset_days/days_header_sub_item.vue'; import DaysHeaderSubItem from 'ee/oncall_schedules/components/schedule/components/preset_days/days_header_sub_item.vue';
import updateShiftTimeUnitWidthMutation from 'ee/oncall_schedules/graphql/mutations/update_shift_time_unit_width.mutation.graphql';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('ee/oncall_schedules/components/schedule/components/preset_days/days_header_sub_item.vue', () => { describe('ee/oncall_schedules/components/schedule/components/preset_days/days_header_sub_item.vue', () => {
...@@ -14,9 +12,6 @@ describe('ee/oncall_schedules/components/schedule/components/preset_days/days_he ...@@ -14,9 +12,6 @@ describe('ee/oncall_schedules/components/schedule/components/preset_days/days_he
propsData: { propsData: {
timeframeItem, timeframeItem,
}, },
directives: {
GlResizeObserver: createMockDirective(),
},
mocks: { mocks: {
$apollo: { $apollo: {
mutate: jest.fn(), mutate: jest.fn(),
...@@ -36,7 +31,6 @@ describe('ee/oncall_schedules/components/schedule/components/preset_days/days_he ...@@ -36,7 +31,6 @@ describe('ee/oncall_schedules/components/schedule/components/preset_days/days_he
} }
}); });
const findDaysHeaderSubItem = () => wrapper.findByTestId('day-item-sublabel');
const findDaysHeaderCurrentIndicator = () => const findDaysHeaderCurrentIndicator = () =>
wrapper.findByTestId('day-item-sublabel-current-indicator'); wrapper.findByTestId('day-item-sublabel-current-indicator');
...@@ -54,25 +48,4 @@ describe('ee/oncall_schedules/components/schedule/components/preset_days/days_he ...@@ -54,25 +48,4 @@ describe('ee/oncall_schedules/components/schedule/components/preset_days/days_he
expect(findDaysHeaderCurrentIndicator().exists()).toBe(true); expect(findDaysHeaderCurrentIndicator().exists()).toBe(true);
}); });
}); });
describe('updateShiftStyles', () => {
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.dailyHourCell[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(findDaysHeaderSubItem().element, 'gl-resize-observer');
value();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(2);
});
});
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
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 { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('WeeksHeaderSubItemComponent', () => { describe('WeeksHeaderSubItemComponent', () => {
...@@ -19,9 +17,6 @@ describe('WeeksHeaderSubItemComponent', () => { ...@@ -19,9 +17,6 @@ describe('WeeksHeaderSubItemComponent', () => {
propsData: { propsData: {
timeframeItem, timeframeItem,
}, },
directives: {
GlResizeObserver: createMockDirective(),
},
mocks: { mocks: {
$apollo: { $apollo: {
mutate: jest.fn(), mutate: jest.fn(),
...@@ -42,7 +37,6 @@ describe('WeeksHeaderSubItemComponent', () => { ...@@ -42,7 +37,6 @@ describe('WeeksHeaderSubItemComponent', () => {
}); });
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', () => {
...@@ -84,24 +78,5 @@ describe('WeeksHeaderSubItemComponent', () => { ...@@ -84,24 +78,5 @@ 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);
});
}); });
}); });
...@@ -70,7 +70,8 @@ describe('RotationsListSectionComponent', () => { ...@@ -70,7 +70,8 @@ describe('RotationsListSectionComponent', () => {
}); });
it('renders timeline cell items based on timeframe data', () => { it('renders timeline cell items based on timeframe data', () => {
expect(findTimelineCells().length).toBe(mockTimeframeWeeks.length); const mockTimelineCellWidth = 1;
expect(findTimelineCells().length).toBe(mockTimelineCellWidth);
}); });
it('renders current day indicator in the first timeline cell', () => { it('renders current day indicator in the first timeline cell', () => {
...@@ -78,7 +79,7 @@ describe('RotationsListSectionComponent', () => { ...@@ -78,7 +79,7 @@ describe('RotationsListSectionComponent', () => {
}); });
it('render the correct amount of rotation assignees with their related information', () => { it('render the correct amount of rotation assignees with their related information', () => {
expect(findRotationAssignees()).toHaveLength(2); expect(findRotationAssignees()).toHaveLength(mockRotations[0].shifts.nodes.length);
expect(findRotationAssignees().at(0).props().assignee.user).toEqual( expect(findRotationAssignees().at(0).props().assignee.user).toEqual(
mockRotations[0].shifts.nodes[0].participant.user, mockRotations[0].shifts.nodes[0].participant.user,
); );
......
import { shallowMount } from '@vue/test-utils';
import RotationsAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import DaysScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/days_schedule_shift.vue';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
import { nDaysAfter } from '~/lib/utils/datetime_utility';
const shift = {
participant: {
id: '1',
user: {
username: 'nora.schaden',
},
},
startsAt: '2021-01-15T00:04:56.333Z',
endsAt: '2021-01-15T04:22:56.333Z',
};
const CELL_WIDTH = 50;
const timeframeItem = new Date(2021, 0, 15);
const timeframe = [timeframeItem, nDaysAfter(timeframeItem, DAYS_IN_WEEK)];
describe('ee/oncall_schedules/components/schedule/components/shifts/components/days_schedule_shift.vue', () => {
let wrapper;
function createComponent({ props = {} } = {}) {
wrapper = shallowMount(DaysScheduleShift, {
propsData: {
shift,
shiftIndex: 0,
timeframeItem,
timeframe,
presetType: PRESET_TYPES.WEEKS,
shiftTimeUnitWidth: CELL_WIDTH,
...props,
},
});
}
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 hours * CELL_WIDTH(5 * 50)
*/
createComponent({ data: { shiftTimeUnitWidth: CELL_WIDTH } });
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '0px',
width: '248px',
});
});
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 500px i.e. ((HOURS_IN_DAY - (HOURS_IN_DAY - overlapStartTime)) * CELL_WIDTH) (((24 - (24 - 10)) * 50))
* and width should be overlapping hours * CELL_WIDTH (12 * 50 - 2)
*/
createComponent({
props: {
shift: {
...shift,
startsAt: '2021-01-15T10:04:56.333Z',
endsAt: '2021-01-15T22:04:56.333Z',
},
},
data: { shiftTimeUnitWidth: CELL_WIDTH },
});
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '500px',
width: '598px',
});
});
it('handles the offset for timezone changes', () => {
const DLSTimeframeItem = new Date(2021, 2, 14);
const DSLTimeframe = [timeframeItem, nDaysAfter(timeframeItem, DAYS_IN_WEEK)];
/**
* Where left should be: ((HOURS_IN_DAY - (HOURS_IN_DAY - overlapStartTime)) * CELL_WIDTH)
* and width should be: (overlappingHours + timezoneOffset) * CELL_WIDTH
*/
createComponent({
props: {
shift: {
...shift,
startsAt: '2021-03-14T05:00:00Z',
endsAt: '2021-03-14T07:00:00Z',
},
timeframeItem: DLSTimeframeItem,
timeframe: DSLTimeframe,
},
data: { shiftTimeUnitWidth: CELL_WIDTH },
});
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '250px',
width: '98px',
});
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DaysScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/days_schedule_shift.vue';
import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue'; import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue';
import WeeksScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/weeks_schedule_shift.vue'; import ShiftItem from 'ee/oncall_schedules/components/schedule/components/shifts/components/shift_item.vue';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
import { nDaysAfter } from '~/lib/utils/datetime_utility'; import { nDaysAfter } from '~/lib/utils/datetime_utility';
import mockRotations from '../../../../mocks/mock_rotation.json'; import mockRotations from '../../../../mocks/mock_rotation.json';
...@@ -23,14 +22,14 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s ...@@ -23,14 +22,14 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
}, },
data() { data() {
return { return {
shiftTimeUnitWidth: 0, timelineWidth: 0,
...data, ...data,
}; };
}, },
mocks: { mocks: {
$apollo: { $apollo: {
queries: { queries: {
shiftTimeUnitWidth: 0, timelineWidth: 0,
}, },
}, },
}, },
...@@ -45,14 +44,13 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s ...@@ -45,14 +44,13 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
wrapper.destroy(); wrapper.destroy();
}); });
const findDaysScheduleShifts = () => wrapper.findAllComponents(DaysScheduleShift); const findShiftItems = () => wrapper.findAllComponents(ShiftItem);
const findWeeksScheduleShifts = () => wrapper.findAllComponents(WeeksScheduleShift);
const updateShifts = (startsAt, endsAt) => const updateShifts = (startsAt, endsAt) =>
mockRotations[0].shifts.nodes.map((el) => ({ ...el, startsAt, endsAt })); mockRotations[0].shifts.nodes.map((el) => ({ ...el, startsAt, endsAt }));
describe('when the preset type is WEEKS', () => { describe('when the preset type is WEEKS', () => {
it('should render a selection of week grid shifts inside the rotation', () => { it('should render a selection of week grid shifts inside the rotation', () => {
expect(findWeeksScheduleShifts()).toHaveLength(2); expect(findShiftItems()).toHaveLength(2);
}); });
it.each` it.each`
...@@ -78,14 +76,14 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s ...@@ -78,14 +76,14 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
}, },
}); });
expect(findWeeksScheduleShifts().exists()).toBe(false); expect(findShiftItems().exists()).toBe(false);
}); });
}); });
describe('when the preset type is DAYS', () => { describe('when the preset type is DAYS', () => {
it('should render a selection of day grid shifts inside the rotation', () => { it('should render a selection of day grid shifts inside the rotation', () => {
createComponent({ props: { presetType: PRESET_TYPES.DAYS } }); createComponent({ props: { presetType: PRESET_TYPES.DAYS } });
expect(findDaysScheduleShifts()).toHaveLength(1); expect(findShiftItems()).toHaveLength(2);
}); });
it.each` it.each`
...@@ -111,7 +109,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s ...@@ -111,7 +109,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
}, },
}); });
expect(findDaysScheduleShifts().exists()).toBe(false); expect(findShiftItems().exists()).toBe(false);
}); });
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import RotationsAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue'; import RotationsAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import WeeksScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/weeks_schedule_shift.vue'; import ShiftItem from 'ee/oncall_schedules/components/schedule/components/shifts/components/shift_item.vue';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
import { nDaysAfter } from '~/lib/utils/datetime_utility'; import { nDaysAfter } from '~/lib/utils/datetime_utility';
...@@ -20,19 +20,15 @@ const CELL_WIDTH = 50; ...@@ -20,19 +20,15 @@ const CELL_WIDTH = 50;
const timeframeItem = new Date(2021, 0, 13); const timeframeItem = new Date(2021, 0, 13);
const timeframe = [timeframeItem, new Date(nDaysAfter(timeframeItem, DAYS_IN_WEEK))]; const timeframe = [timeframeItem, new Date(nDaysAfter(timeframeItem, DAYS_IN_WEEK))];
describe('ee/oncall_schedules/components/schedule/components/shifts/components/weeks_schedule_shift.vue', () => { describe('ee/oncall_schedules/components/schedule/components/shifts/components/shift_item.vue', () => {
let wrapper; let wrapper;
function createComponent({ props = {} } = {}) { function createComponent({ props = {} } = {}) {
wrapper = shallowMount(WeeksScheduleShift, { wrapper = shallowMount(ShiftItem, {
propsData: { propsData: {
shift, shift,
shiftIndex: 0,
timeframeItem,
timeframe, timeframe,
presetType: PRESET_TYPES.WEEKS, presetType: PRESET_TYPES.WEEKS,
shiftTimeUnitWidth: CELL_WIDTH,
rotationLength: { lengthUnit: 'DAYS' },
timelineWidth: CELL_WIDTH * 14, timelineWidth: CELL_WIDTH * 14,
...props, ...props,
}, },
...@@ -53,39 +49,6 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w ...@@ -53,39 +49,6 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w
it('should render a rotation assignee child component', () => { it('should render a rotation assignee child component', () => {
expect(findRotationAssignee().exists()).toBe(true); 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 absolute pixel width (3.5 * CELL_WIDTH)
*/
createComponent({ data: { shiftTimeUnitWidth: CELL_WIDTH } });
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '0px',
width: '175px',
});
});
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 52x i.e. ((DAYS_IN_WEEK - (timeframeEndsAt - overlapStartDate)) * CELL_WIDTH)(((7 - (20 - 14)) * 50))
* and width should be absolute pixel width (3.5 * CELL_WIDTH)
*/
createComponent({
props: {
shift: {
...shift,
startsAt: '2021-01-14T10:04:56.333Z',
endsAt: '2021-01-17T22:04:56.333Z',
},
},
data: { shiftTimeUnitWidth: CELL_WIDTH },
});
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '50px',
width: '175px',
});
});
}); });
describe('shift overlaps inside the current time-frame with a shift equal to 24 hours', () => { describe('shift overlaps inside the current time-frame with a shift equal to 24 hours', () => {
...@@ -99,17 +62,6 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w ...@@ -99,17 +62,6 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w
it('should render a rotation assignee child component', () => { it('should render a rotation assignee child component', () => {
expect(findRotationAssignee().exists()).toBe(true); expect(findRotationAssignee().exists()).toBe(true);
}); });
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 ((DAYS_IN_WEEK - (timeframeEndsAt - overlapStartDate)) * CELL_WIDTH)(((7 - (20 - 14)) * 50))
* and width should be absolute pixel width (1.5 * CELL_WIDTH)
*/
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '50px',
width: '75px',
});
});
}); });
describe('shift overlaps inside the current time-frame with a shift less than 24 hours', () => { describe('shift overlaps inside the current time-frame with a shift less than 24 hours', () => {
...@@ -130,16 +82,5 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w ...@@ -130,16 +82,5 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w
it('should render a rotation assignee child component', () => { it('should render a rotation assignee child component', () => {
expect(findRotationAssignee().exists()).toBe(true); expect(findRotationAssignee().exists()).toBe(true);
}); });
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 70px i.e. ((CELL_WIDTH / HOURS_IN_DAY) * overlapStartDate + dayOffSet)(50 / 24 * 10) + 50;
* and width should be the correct fraction of a day: (hours / 24) * CELL_WIDTH
*/
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '70px',
width: '4px',
});
});
}); });
}); });
...@@ -97,7 +97,7 @@ describe('Schedule Common Mixins', () => { ...@@ -97,7 +97,7 @@ describe('Schedule Common Mixins', () => {
const leftOffset = oneDayOffsetWeekView * daysSinceShiftStart + hourOffset; const leftOffset = oneDayOffsetWeekView * daysSinceShiftStart + hourOffset;
expect(wrapper.vm.getIndicatorStyles(PRESET_TYPES.WEEKS, mockTimeframeInitialDate)).toEqual( expect(wrapper.vm.getIndicatorStyles(PRESET_TYPES.WEEKS, mockTimeframeInitialDate)).toEqual(
expect.objectContaining({ expect.objectContaining({
left: `${Math.round(leftOffset)}%`, left: `${leftOffset}%`,
}), }),
); );
}); });
......
...@@ -17027,9 +17027,6 @@ msgstr "" ...@@ -17027,9 +17027,6 @@ 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 ""
......
...@@ -966,62 +966,6 @@ describe('format24HourTimeStringFromInt', () => { ...@@ -966,62 +966,6 @@ describe('format24HourTimeStringFromInt', () => {
}); });
}); });
describe('getOverlapDateInPeriods', () => {
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, the amount of hours overlapping, start date of overlap and end date of overlap', () => {
expect(
datetimeUtility.getOverlapDateInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
).toEqual({
daysOverlap: 2,
hoursOverlap: 48,
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.getOverlapDateInPeriods(
{ 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.getOverlapDateInPeriods({ start, end }, { start: startInvalid, end }),
).toThrow(error);
});
it('throws an exception when the right period contains an invalid date', () => {
expect(() =>
datetimeUtility.getOverlapDateInPeriods({ start, end }, { start, end: endInvalid }),
).toThrow(error);
});
});
});
describe('isToday', () => { describe('isToday', () => {
const today = new Date(); const today = new Date();
it.each` it.each`
......
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