Commit c729dc18 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '262860-draw-rotation-to-grid-hour' into 'master'

Add base hour view for oncall schedule grid

See merge request gitlab-org/gitlab!52006
parents a78622c9 0e71ebdc
...@@ -4,7 +4,8 @@ import * as timeago from 'timeago.js'; ...@@ -4,7 +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; const MILLISECONDS_IN_HOUR = 60 * 60 * 1000;
const MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR;
window.timeago = timeago; window.timeago = timeago;
...@@ -859,17 +860,17 @@ export const format24HourTimeStringFromInt = (time) => { ...@@ -859,17 +860,17 @@ export const format24HourTimeStringFromInt = (time) => {
* *
* @param {Object} givenPeriodLeft - the first period to compare. * @param {Object} givenPeriodLeft - the first period to compare.
* @param {Object} givenPeriodRight - the second 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 } * @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 * @throws {Error} Uncaught Error: Invalid period
* *
* @example * @example
* getOverlappingDaysInPeriods( * getOverlapDateInPeriods(
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) }, * { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) },
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) } * { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) }
* ) => { daysOverlap: 2, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 } * ) => { daysOverlap: 2, hoursOverlap: 48, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
* *
*/ */
export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => { export const getOverlapDateInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => {
const leftStartTime = new Date(givenPeriodLeft.start).getTime(); const leftStartTime = new Date(givenPeriodLeft.start).getTime();
const leftEndTime = new Date(givenPeriodLeft.end).getTime(); const leftEndTime = new Date(givenPeriodLeft.end).getTime();
const rightStartTime = new Date(givenPeriodRight.start).getTime(); const rightStartTime = new Date(givenPeriodRight.start).getTime();
...@@ -890,6 +891,7 @@ export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRig ...@@ -890,6 +891,7 @@ export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRig
const differenceInMs = overlapEndDate - overlapStartDate; const differenceInMs = overlapEndDate - overlapStartDate;
return { return {
hoursOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_HOUR),
daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY), daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY),
overlapStartDate, overlapStartDate,
overlapEndDate, overlapEndDate,
......
...@@ -85,9 +85,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi ...@@ -85,9 +85,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
} }
.timeline-header-item { .timeline-header-item {
// container size minus left panel width divided by 2 week timeframes
width: calc((100% - #{$details-cell-width}) / 2);
&:last-of-type .item-label { &:last-of-type .item-label {
@include gl-border-r-0; @include gl-border-r-0;
} }
...@@ -168,9 +165,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi ...@@ -168,9 +165,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
.timeline-cell { .timeline-cell {
@include gl-relative; @include gl-relative;
// width: $timeline-cell-width;
// container size minus left panel width divided by 2 week timeframes
width: calc((100% - #{$details-cell-width}) / 2);
@include gl-bg-transparent; @include gl-bg-transparent;
border-right: $border-style; border-right: $border-style;
......
...@@ -4,9 +4,12 @@ import { ...@@ -4,9 +4,12 @@ import {
GlCard, GlCard,
GlButtonGroup, GlButtonGroup,
GlButton, GlButton,
GlDropdown,
GlDropdownItem,
GlModalDirective, GlModalDirective,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue'; import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue';
...@@ -33,17 +36,19 @@ export default { ...@@ -33,17 +36,19 @@ export default {
editRotationModalId, editRotationModalId,
editScheduleModalId, editScheduleModalId,
deleteScheduleModalId, deleteScheduleModalId,
presetType: PRESET_TYPES.WEEKS, PRESET_TYPES,
components: { components: {
GlSprintf,
GlCard,
ScheduleTimelineSection,
GlButtonGroup,
GlButton, GlButton,
GlButtonGroup,
GlCard,
GlDropdown,
GlDropdownItem,
GlSprintf,
AddEditRotationModal,
DeleteScheduleModal, DeleteScheduleModal,
EditScheduleModal, EditScheduleModal,
AddEditRotationModal,
RotationsListSection, RotationsListSection,
ScheduleTimelineSection,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -61,6 +66,11 @@ export default { ...@@ -61,6 +66,11 @@ export default {
default: () => [], default: () => [],
}, },
}, },
data() {
return {
presetType: this.$options.PRESET_TYPES.WEEKS,
};
},
computed: { computed: {
offset() { offset() {
const selectedTz = this.timezones.find((tz) => tz.identifier === this.schedule.timezone); const selectedTz = this.timezones.find((tz) => tz.identifier === this.schedule.timezone);
...@@ -70,11 +80,23 @@ export default { ...@@ -70,11 +80,23 @@ export default {
return getTimeframeForWeeksView(); return getTimeframeForWeeksView();
}, },
scheduleRange() { scheduleRange() {
const range = { start: this.timeframe[0], end: this.timeframe[this.timeframe.length - 1] }; const end =
this.presetType === this.$options.PRESET_TYPES.DAYS
? this.timeframe[0]
: this.timeframe[this.timeframe.length - 1];
const range = { start: this.timeframe[0], end };
return `${formatDate(range.start, 'mmmm d')} - ${formatDate(range.end, 'mmmm d, yyyy')}`; return `${formatDate(range.start, 'mmmm d')} - ${formatDate(range.end, 'mmmm d, yyyy')}`;
}, },
}, },
methods: {
setPresetType(type) {
this.presetType = type;
},
formatPresetType(type) {
return capitalize(type);
},
},
}; };
</script> </script>
...@@ -105,11 +127,24 @@ export default { ...@@ -105,11 +127,24 @@ export default {
</gl-button-group> </gl-button-group>
</div> </div>
</template> </template>
<p class="gl-text-gray-500 gl-mb-3" data-testid="scheduleBody"> <p
class="gl-text-gray-500 gl-mb-3 gl-display-flex gl-justify-content-space-between gl-align-items-center"
data-testid="scheduleBody"
>
<gl-sprintf :message="$options.i18n.scheduleForTz"> <gl-sprintf :message="$options.i18n.scheduleForTz">
<template #timezone>{{ schedule.timezone }}</template> <template #timezone>{{ schedule.timezone }}</template>
</gl-sprintf> </gl-sprintf>
| {{ offset }} | {{ offset }}
<gl-dropdown right :text="formatPresetType(presetType)">
<gl-dropdown-item
v-for="type in $options.PRESET_TYPES"
:key="type"
:is-check-item="true"
:is-checked="type === presetType"
@click="setPresetType(type)"
>{{ formatPresetType(type) }}</gl-dropdown-item
>
</gl-dropdown>
</p> </p>
<div class="gl-w-full gl-display-flex gl-align-items-center gl-pb-3"> <div class="gl-w-full gl-display-flex gl-align-items-center gl-pb-3">
<gl-button-group> <gl-button-group>
...@@ -133,9 +168,9 @@ export default { ...@@ -133,9 +168,9 @@ export default {
</template> </template>
<div class="schedule-shell" data-testid="rotationsBody"> <div class="schedule-shell" data-testid="rotationsBody">
<schedule-timeline-section :preset-type="$options.presetType" :timeframe="timeframe" /> <schedule-timeline-section :preset-type="presetType" :timeframe="timeframe" />
<rotations-list-section <rotations-list-section
:preset-type="$options.presetType" :preset-type="presetType"
:rotations="rotations" :rotations="rotations"
:timeframe="timeframe" :timeframe="timeframe"
/> />
......
...@@ -5,9 +5,9 @@ import { __, sprintf } from '~/locale'; ...@@ -5,9 +5,9 @@ import { __, sprintf } from '~/locale';
export default { export default {
components: { components: {
GlToken,
GlAvatarLabeled, GlAvatarLabeled,
GlPopover, GlPopover,
GlToken,
}, },
props: { props: {
assignee: { assignee: {
...@@ -33,12 +33,12 @@ export default { ...@@ -33,12 +33,12 @@ export default {
}, },
startsAt() { startsAt() {
return sprintf(__('Starts: %{startsAt}'), { return sprintf(__('Starts: %{startsAt}'), {
startsAt: formatDate(this.rotationAssigneeStartsAt, 'mmmm d, yyyy, hh:mm'), startsAt: formatDate(this.rotationAssigneeStartsAt, 'mmmm d, yyyy, h:MMtt Z'),
}); });
}, },
endsAt() { endsAt() {
return sprintf(__('Ends: %{endsAt}'), { return sprintf(__('Ends: %{endsAt}'), {
endsAt: formatDate(this.rotationAssigneeEndsAt, 'mmmm d, yyyy, hh:mm'), endsAt: formatDate(this.rotationAssigneeEndsAt, 'mmmm d, yyyy, h:MMtt Z'),
}); });
}, },
}, },
......
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
<template> <template>
<span <span
v-if="hasToday" v-if="hasToday"
:style="getIndicatorStyles()" :style="getIndicatorStyles(presetType)"
data-testid="current-day-indicator" data-testid="current-day-indicator"
class="current-day-indicator" class="current-day-indicator"
></span> ></span>
......
<script>
import { PRESET_TYPES, TIMELINE_CELL_WIDTH } from 'ee/oncall_schedules/constants';
import { formatDate } from '~/lib/utils/datetime_utility';
import DaysHeaderSubItem from './days_header_sub_item.vue';
export default {
PRESET_TYPES,
components: {
DaysHeaderSubItem,
},
props: {
timeframeItem: {
type: Date,
required: true,
},
},
computed: {
timelineHeaderLabel() {
return formatDate(this.timeframeItem, 'mmmm d, yyyy');
},
timelineHeaderStyles() {
return {
width: `calc(${100}% - ${TIMELINE_CELL_WIDTH}px)`,
};
},
},
};
</script>
<template>
<span class="timeline-header-item" :style="timelineHeaderStyles">
<div class="item-label gl-pl-6 gl-py-4" data-testid="timeline-header-label">
{{ timelineHeaderLabel }}
</div>
<days-header-sub-item />
</span>
</template>
<script>
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 { GlResizeObserverDirective } from '@gitlab/ui';
export default {
PRESET_TYPES,
HOURS_IN_DAY,
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [CommonMixin],
mounted() {
this.updateShiftStyles();
},
methods: {
updateShiftStyles() {
this.$apollo.mutate({
mutation: updateShiftTimeUnitWidthMutation,
variables: {
shiftTimeUnitWidth: this.$refs.dailyHourCell[0].offsetWidth,
},
});
},
},
};
</script>
<template>
<div
v-gl-resize-observer="updateShiftStyles"
class="item-sublabel"
data-testid="day-item-sublabel"
>
<span
v-for="hour in $options.HOURS_IN_DAY"
:key="hour"
ref="dailyHourCell"
class="sublabel-value"
data-testid="sublabel-value"
>{{ hour }}</span
>
<span
:style="getIndicatorStyles($options.PRESET_TYPES.DAYS)"
class="current-day-indicator-header preset-days"
data-testid="day-item-sublabel-current-indicator"
></span>
</div>
</template>
<script> <script>
import { TIMELINE_CELL_WIDTH } from 'ee/oncall_schedules/constants';
import CommonMixin from 'ee/oncall_schedules/mixins/common_mixin';
import { monthInWords } from '~/lib/utils/datetime_utility'; import { monthInWords } from '~/lib/utils/datetime_utility';
import WeeksHeaderSubItem from './weeks_header_sub_item.vue'; import WeeksHeaderSubItem from './weeks_header_sub_item.vue';
import CommonMixin from '../../../../mixins/common_mixin';
export default { export default {
components: { components: {
...@@ -52,12 +53,17 @@ export default { ...@@ -52,12 +53,17 @@ export default {
return ''; return '';
}, },
timelineHeaderStyles() {
return {
width: `calc((${100}% - ${TIMELINE_CELL_WIDTH}px) / ${2})`,
};
},
}, },
}; };
</script> </script>
<template> <template>
<span class="timeline-header-item"> <span class="timeline-header-item" :style="timelineHeaderStyles">
<div <div
:class="timelineHeaderClass" :class="timelineHeaderClass"
class="item-label gl-pl-6 gl-py-4" class="item-label gl-pl-6 gl-py-4"
......
<script> <script>
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import updateShiftTimeUnitWidthMutation from 'ee/oncall_schedules/graphql/mutations/update_shift_time_unit_width.mutation.graphql'; 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';
import { GlResizeObserverDirective } from '@gitlab/ui'; import { GlResizeObserverDirective } from '@gitlab/ui';
export default { export default {
PRESET_TYPES,
directives: { directives: {
GlResizeObserver: GlResizeObserverDirective, GlResizeObserver: GlResizeObserverDirective,
}, },
...@@ -44,6 +46,9 @@ export default { ...@@ -44,6 +46,9 @@ export default {
} }
return ''; return '';
}, },
getSubItemValue(subItem) {
return subItem.getDate();
},
updateShiftStyles() { updateShiftStyles() {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: updateShiftTimeUnitWidthMutation, mutation: updateShiftTimeUnitWidthMutation,
...@@ -69,11 +74,11 @@ export default { ...@@ -69,11 +74,11 @@ export default {
:class="getSubItemValueClass(subItem)" :class="getSubItemValueClass(subItem)"
class="sublabel-value" class="sublabel-value"
data-testid="sublabel-value" data-testid="sublabel-value"
>{{ subItem.getDate() }}</span >{{ getSubItemValue(subItem) }}</span
> >
<span <span
v-if="hasToday" v-if="hasToday"
:style="getIndicatorStyles()" :style="getIndicatorStyles($options.PRESET_TYPES.WEEKS)"
class="current-day-indicator-header preset-weeks" class="current-day-indicator-header preset-weeks"
></span> ></span>
</div> </div>
......
<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 DeleteRotationModal from 'ee/oncall_schedules/components/rotations/components/delete_rotation_modal.vue';
import { editRotationModalId, deleteRotationModalId } from 'ee/oncall_schedules/constants'; import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue';
import {
editRotationModalId,
deleteRotationModalId,
PRESET_TYPES,
TIMELINE_CELL_WIDTH,
} 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 ScheduleShift from './schedule_shift.vue';
export const i18n = { export const i18n = {
editRotationLabel: s__('OnCallSchedules|Edit rotation'), editRotationLabel: s__('OnCallSchedules|Edit rotation'),
...@@ -16,11 +21,11 @@ export default { ...@@ -16,11 +21,11 @@ export default {
editRotationModalId, editRotationModalId,
deleteRotationModalId, deleteRotationModalId,
components: { components: {
GlButtonGroup,
GlButton, GlButton,
GlButtonGroup,
CurrentDayIndicator, CurrentDayIndicator,
DeleteRotationModal, DeleteRotationModal,
ScheduleShift, ScheduleShiftWrapper,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -43,15 +48,33 @@ export default { ...@@ -43,15 +48,33 @@ export default {
data() { data() {
return { return {
rotationToUpdate: {}, rotationToUpdate: {},
shiftWidths: 0,
}; };
}, },
computed: {
presetIsDay() {
return this.presetType === PRESET_TYPES.DAYS;
},
timeframeToDraw() {
if (this.presetIsDay) {
return [this.timeframe[0]];
}
return this.timeframe;
},
timelineStyles() {
const length = this.presetIsDay ? 1 : 2;
return {
width: `calc((${100}% - ${TIMELINE_CELL_WIDTH}px) / ${length})`,
};
},
},
methods: { methods: {
setRotationToUpdate(rotation) { setRotationToUpdate(rotation) {
this.rotationToUpdate = rotation; this.rotationToUpdate = rotation;
}, },
isLastCell(index) { cellShouldHideOverflow(index) {
return index + 1 === this.timeframe.length; return index + 1 === this.timeframe.length || this.presetIsDay;
}, },
}, },
}; };
...@@ -89,21 +112,19 @@ export default { ...@@ -89,21 +112,19 @@ export default {
</gl-button-group> </gl-button-group>
</span> </span>
<span <span
v-for="(timeframeItem, index) in timeframe" v-for="(timeframeItem, index) in timeframeToDraw"
:key="index" :key="index"
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"
:class="{ 'gl-overflow-hidden': isLastCell(index) }" :class="{ 'gl-overflow-hidden': cellShouldHideOverflow(index) }"
:style="timelineStyles"
data-testid="timelineCell" data-testid="timelineCell"
> >
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" /> <current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<schedule-shift <schedule-shift-wrapper
v-for="(shift, shiftIndex) in rotation.shifts.nodes"
:key="shift.startAt"
:shift="shift"
:shift-index="shiftIndex"
:preset-type="presetType" :preset-type="presetType"
:timeframe-item="timeframeItem" :timeframe-item="timeframeItem"
:timeframe="timeframe" :timeframe="timeframe"
:rotation="rotation"
/> />
</span> </span>
</div> </div>
......
<script> <script>
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import DaysHeaderItem from './preset_days/days_header_item.vue';
import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue'; import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue';
export default { export default {
PRESET_TYPES,
components: { components: {
DaysHeaderItem,
WeeksHeaderItem, WeeksHeaderItem,
}, },
props: { props: {
...@@ -15,18 +19,28 @@ export default { ...@@ -15,18 +19,28 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
presetIsDay() {
return this.presetType === this.$options.PRESET_TYPES.DAYS;
},
},
}; };
</script> </script>
<template> <template>
<div class="timeline-section clearfix"> <div class="timeline-section clearfix">
<span class="timeline-header-blank"></span> <span class="timeline-header-blank"></span>
<weeks-header-item <div>
v-for="(timeframeItem, index) in timeframe" <days-header-item v-if="presetIsDay" :timeframe-item="timeframe[0]" />
:key="index" <weeks-header-item
:timeframe-index="index" v-for="(timeframeItem, index) in timeframe"
:timeframe-item="timeframeItem" v-else
:timeframe="timeframe" :key="index"
/> :timeframe-index="index"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:preset-type="presetType"
/>
</div>
</div> </div>
</template> </template>
<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 { 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,
},
shiftTimeUnitWidth: {
type: Number,
required: true,
},
},
computed: {
currentTimeframeEndsAt() {
return incrementDateByDays(this.timeframeItem, 1);
},
hoursUntilEndOfTimeFrame() {
return HOURS_IN_DAY - new Date(this.shiftRangeOverlap.overlapStartDate).getHours();
},
rotationAssigneeStyle() {
const startHour = this.shiftStartsAt.getHours();
const isFirstCell = startHour === 0;
const shouldStartAtBeginningOfCell = isFirstCell || this.shiftStartHourOutOfRange;
const widthOffset = shouldStartAtBeginningOfCell ? 0 : 1;
const width =
this.shiftEndsAt.getTime() > this.currentTimeframeEndsAt.getTime()
? HOURS_IN_DAY
: this.shiftRangeOverlap.hoursOverlap + widthOffset;
const left = shouldStartAtBeginningOfCell
? '0px'
: `${(23 - this.hoursUntilEndOfTimeFrame) * this.shiftTimeUnitWidth + ASSIGNEE_SPACER}px`;
return {
left,
width: `${this.shiftTimeUnitWidth * width}px`,
};
},
shiftStartsAt() {
return new Date(this.shift.startsAt);
},
shiftEndsAt() {
return new Date(this.shift.endsAt);
},
shiftRangeOverlap() {
try {
return getOverlapDateInPeriods(
{ start: this.timeframeItem, end: this.currentTimeframeEndsAt },
{ start: this.shiftStartsAt, end: this.shiftEndsAt },
);
} catch (error) {
return { hoursOverlap: 0 };
}
},
shiftStartHourOutOfRange() {
return this.shiftStartsAt.getTime() < this.timeframeItem.getTime();
},
shiftShouldRender() {
return Boolean(
this.shiftRangeOverlap.hoursOverlap &&
!(this.shiftStartsAt.getDate() > this.timeframeItem.getDate()),
);
},
},
};
</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>
<script>
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import getShiftTimeUnitWidthQuery from 'ee/oncall_schedules/graphql/queries/get_shift_time_unit_width.query.graphql';
import DaysScheduleShift from './days_schedule_shift.vue';
import WeeksScheduleShift from './weeks_schedule_shift.vue';
export default {
components: {
DaysScheduleShift,
WeeksScheduleShift,
},
props: {
timeframeItem: {
type: [Date, Object],
required: true,
},
timeframe: {
type: Array,
required: true,
},
presetType: {
type: String,
required: true,
},
rotation: {
type: Object,
required: true,
},
},
data() {
return {
shiftTimeUnitWidth: 0,
componentByPreset: {
[PRESET_TYPES.DAYS]: DaysScheduleShift,
[PRESET_TYPES.WEEKS]: WeeksScheduleShift,
},
};
},
apollo: {
shiftTimeUnitWidth: {
query: getShiftTimeUnitWidthQuery,
},
},
};
</script>
<template>
<div>
<component
:is="componentByPreset[presetType]"
v-for="(shift, shiftIndex) in rotation.shifts.nodes"
:key="shift.startAt"
:shift="shift"
:shift-index="shiftIndex"
:preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:shift-time-unit-width="shiftTimeUnitWidth"
/>
</div>
</template>
<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 { import { DAYS_IN_WEEK, DAYS_IN_DATE_WEEK, ASSIGNEE_SPACER } from 'ee/oncall_schedules/constants';
PRESET_TYPES, import { getOverlapDateInPeriods } from '~/lib/utils/datetime_utility';
DAYS_IN_WEEK, import { incrementDateByDays } from '../../../utils';
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 { export default {
components: { components: {
...@@ -35,25 +29,14 @@ export default { ...@@ -35,25 +29,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
},
data() {
return {
shiftTimeUnitWidth: 0,
};
},
apollo: {
shiftTimeUnitWidth: { shiftTimeUnitWidth: {
query: getShiftTimeUnitWidthQuery, type: Number,
required: true,
}, },
}, },
computed: { computed: {
currentTimeframeEndsAt() { currentTimeframeEndsAt() {
let UnitOfIncrement = 0; return incrementDateByDays(this.timeframeItem, DAYS_IN_DATE_WEEK);
if (this.presetType === PRESET_TYPES.WEEKS) {
UnitOfIncrement = DAYS_IN_DATE_WEEK;
}
return incrementDateByDays(this.timeframeItem, UnitOfIncrement);
}, },
daysUntilEndOfTimeFrame() { daysUntilEndOfTimeFrame() {
return ( return (
...@@ -101,12 +84,11 @@ export default { ...@@ -101,12 +84,11 @@ export default {
}, },
shiftRangeOverlap() { shiftRangeOverlap() {
try { try {
return getOverlappingDaysInPeriods( return getOverlapDateInPeriods(
{ start: this.timeframeItem, end: this.currentTimeframeEndsAt }, { start: this.timeframeItem, end: this.currentTimeframeEndsAt },
{ start: this.shiftStartsAt, end: this.shiftEndsAt }, { start: this.shiftStartsAt, end: this.shiftEndsAt },
); );
} catch (error) { } catch (error) {
// TODO: We need to decide the UX implications of a invalid date creation.
return { daysOverlap: 0 }; return { daysOverlap: 0 };
} }
}, },
...@@ -126,7 +108,7 @@ export default { ...@@ -126,7 +108,7 @@ export default {
return this.timeframe[this.timeframe.length - 1]; return this.timeframe[this.timeframe.length - 1];
}, },
totalShiftRangeOverlap() { totalShiftRangeOverlap() {
return getOverlappingDaysInPeriods( return getOverlapDateInPeriods(
{ {
start: this.timeframeItem, start: this.timeframeItem,
end: incrementDateByDays(this.timeFrameEndsAt, DAYS_IN_DATE_WEEK), end: incrementDateByDays(this.timeFrameEndsAt, DAYS_IN_DATE_WEEK),
......
...@@ -12,6 +12,7 @@ export const DAYS_IN_WEEK = 7; ...@@ -12,6 +12,7 @@ export const DAYS_IN_WEEK = 7;
export const HOURS_IN_DAY = 24; export const HOURS_IN_DAY = 24;
export const PRESET_TYPES = { export const PRESET_TYPES = {
DAYS: 'DAYS',
WEEKS: 'WEEKS', WEEKS: 'WEEKS',
}; };
...@@ -30,3 +31,4 @@ export const deleteRotationModalId = 'deleteRotationModal'; ...@@ -30,3 +31,4 @@ export const deleteRotationModalId = 'deleteRotationModal';
*/ */
export const DAYS_IN_DATE_WEEK = 6; export const DAYS_IN_DATE_WEEK = 6;
export const ASSIGNEE_SPACER = 2; export const ASSIGNEE_SPACER = 2;
export const TIMELINE_CELL_WIDTH = 180;
import { DAYS_IN_WEEK } from '../constants'; import { DAYS_IN_WEEK, HOURS_IN_DAY, PRESET_TYPES } from '../constants';
export default { export default {
currentDate: null, currentDate: null,
...@@ -29,12 +29,19 @@ export default { ...@@ -29,12 +29,19 @@ export default {
this.$options.currentDate = currentDate; this.$options.currentDate = currentDate;
}, },
methods: { methods: {
getIndicatorStyles() { getIndicatorStyles(presetType = PRESET_TYPES.WEEKS) {
// as we start schedule scale from the current date the indicator will always be on the first date. So we find if (presetType === PRESET_TYPES.DAYS) {
// the percentage of space one day cell takes and divide it by 2 cause the tick is in the middle of the cell. const currentDate = new Date();
// It might be updated to more precise position - time of the day const base = 100 / HOURS_IN_DAY;
const left = 100 / DAYS_IN_WEEK / 2; const hours = base * currentDate.getHours();
const minutes = base * (currentDate.getMinutes() / 60) - 2.25;
return {
left: `${hours + minutes}%`,
};
}
const left = 100 / DAYS_IN_WEEK / 2;
return { return {
left: `${left}%`, left: `${left}%`,
}; };
......
...@@ -16,7 +16,7 @@ describe('RotationAssignee', () => { ...@@ -16,7 +16,7 @@ describe('RotationAssignee', () => {
const findEndsAt = () => wrapper.findByTestId('rotation-assignee-ends-at'); const findEndsAt = () => wrapper.findByTestId('rotation-assignee-ends-at');
const formattedDate = (date) => { const formattedDate = (date) => {
return formatDate(date, 'mmmm d, yyyy, hh:mm'); return formatDate(date, 'mmmm d, yyyy, h:MMtt Z');
}; };
function createComponent() { function createComponent() {
......
...@@ -74,146 +74,148 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders ...@@ -74,146 +74,148 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders
style="left: 7.142857142857143%;" style="left: 7.142857142857143%;"
/> />
<div <div>
class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden" <div
style="left: 0px; width: 0px;" class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden"
> style="left: 0px; width: 0px;"
<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 <span
class="gl-token-content" 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"
> >
<div <span
class="gl-avatar-labeled" class="gl-token-content"
shape="circle"
size="16"
title="nora.schaden"
> >
<div <div
class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s16 gl-avatar-identicon-bg1" class="gl-avatar-labeled"
shape="circle"
size="16"
title="nora.schaden" 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>
<div
class="gl-avatar-labeled-labels gl-text-left!"
>
<div <div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-text-left! gl-mx-n1 gl-my-n1" 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>
</div>
<span <span
class="gl-avatar-labeled-label" class="gl-avatar-labeled-sublabel"
> >
nora.schaden
</span> </span>
</div> </div>
<span
class="gl-avatar-labeled-sublabel"
>
</span>
</div> </div>
</div>
<!---->
<!----> </span>
</span> </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 <div
class="gl-m-0" class="gl-popover"
data-testid="rotation-assignee-ends-at" title="nora.schaden"
> >
Ends: January 15, 2021, 10:01 <p
</p> class="gl-m-0"
data-testid="rotation-assignee-starts-at"
>
Starts: January 12, 2021, 10:04am GMT+0000
</p>
<p
class="gl-m-0"
data-testid="rotation-assignee-ends-at"
>
Ends: January 15, 2021, 10:04am GMT+0000
</p>
</div>
</div> </div>
</div> <div
<div class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden"
class="gl-absolute gl-h-7 gl-mt-3 gl-z-index-1 gl-overflow-hidden" style="left: 2px; width: 0px;"
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 <span
class="gl-token-content" 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"
> >
<div <span
class="gl-avatar-labeled" class="gl-token-content"
shape="circle"
size="16"
title="racheal.loving"
> >
<div <div
class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s16 gl-avatar-identicon-bg1" class="gl-avatar-labeled"
shape="circle"
size="16"
title="racheal.loving" 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>
<div
class="gl-avatar-labeled-labels gl-text-left!"
>
<div <div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-text-left! gl-mx-n1 gl-my-n1" 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 <span
class="gl-avatar-labeled-label" class="gl-avatar-labeled-sublabel"
> >
racheal.loving
</span> </span>
</div> </div>
<span
class="gl-avatar-labeled-sublabel"
>
</span>
</div> </div>
</div>
<!---->
<!----> </span>
</span> </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 <div
class="gl-m-0" class="gl-popover"
data-testid="rotation-assignee-ends-at" title="racheal.loving"
> >
Ends: January 18, 2021, 10:01 <p
</p> class="gl-m-0"
data-testid="rotation-assignee-starts-at"
>
Starts: January 16, 2021, 10:04am GMT+0000
</p>
<p
class="gl-m-0"
data-testid="rotation-assignee-ends-at"
>
Ends: January 18, 2021, 10:04am GMT+0000
</p>
</div>
</div> </div>
</div> </div>
</span> </span>
...@@ -223,8 +225,10 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders ...@@ -223,8 +225,10 @@ exports[`RotationsListSectionComponent when the timeframe includes today renders
> >
<!----> <!---->
<!----> <div>
<!----> <!---->
<!---->
</div>
</span> </span>
</div> </div>
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
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';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES, DAYS_IN_WEEK, HOURS_IN_DAY } from 'ee/oncall_schedules/constants';
describe('CurrentDayIndicator', () => { describe('CurrentDayIndicator', () => {
let wrapper; let wrapper;
...@@ -12,11 +12,12 @@ describe('CurrentDayIndicator', () => { ...@@ -12,11 +12,12 @@ describe('CurrentDayIndicator', () => {
// current indicator will be rendered // current indicator will be rendered
const mockTimeframeInitialDate = new Date(2018, 0, 1); const mockTimeframeInitialDate = new Date(2018, 0, 1);
function createComponent() { function createComponent({
props = { presetType: PRESET_TYPES.WEEKS, timeframeItem: mockTimeframeInitialDate },
} = {}) {
wrapper = shallowMount(CurrentDayIndicator, { wrapper = shallowMount(CurrentDayIndicator, {
propsData: { propsData: {
presetType: PRESET_TYPES.WEEKS, ...props,
timeframeItem: mockTimeframeInitialDate,
}, },
}); });
} }
...@@ -35,8 +36,20 @@ describe('CurrentDayIndicator', () => { ...@@ -35,8 +36,20 @@ describe('CurrentDayIndicator', () => {
expect(wrapper.classes('current-day-indicator')).toBe(true); expect(wrapper.classes('current-day-indicator')).toBe(true);
}); });
it('sets correct styles', async () => { it('sets correct styles for a week', async () => {
const left = 100 / DAYS_IN_WEEK / 2; const left = 100 / DAYS_IN_WEEK / 2;
expect(wrapper.attributes('style')).toBe(`left: ${left}%;`); expect(wrapper.attributes('style')).toBe(`left: ${left}%;`);
}); });
it('sets correct styles for a day', async () => {
createComponent({
props: { presetType: PRESET_TYPES.DAYS, timeframeItem: new Date(2018, 0, 3) },
});
const currentDate = new Date();
const base = 100 / HOURS_IN_DAY;
const hours = base * currentDate.getHours();
const minutes = base * (currentDate.getMinutes() / 60) - 2.25;
const left = hours + minutes;
expect(wrapper.attributes('style')).toBe(`left: ${left}%;`);
});
}); });
import { shallowMount } from '@vue/test-utils';
import DaysHeaderItem from 'ee/oncall_schedules/components/schedule/components/preset_days/days_header_item.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
describe('ee/oncall_schedules/components/schedule/components/preset_days/days_header_item.vue', () => {
let wrapper;
// January 3rd, 2018 - current date (faked)
useFakeDate(2018, 0, 3);
const mockTimeframeInitialDate = new Date(2018, 0, 1);
function mountComponent({ timeframeItem = mockTimeframeInitialDate } = {}) {
wrapper = extendedWrapper(
shallowMount(DaysHeaderItem, {
propsData: {
timeframeItem,
},
}),
);
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
const findHeaderLabel = () => wrapper.findByTestId('timeline-header-label');
describe('timelineHeaderLabel', () => {
it('returns string containing Year, Month and Date for the current timeframe item', () => {
expect(findHeaderLabel().text()).toBe('January 1, 2018');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('ee/oncall_schedules/components/schedule/components/preset_days/days_header_sub_item.vue', () => {
let wrapper;
function mountComponent() {
wrapper = extendedWrapper(
shallowMount(DaysHeaderSubItem, {
propsData: {},
directives: {
GlResizeObserver: createMockDirective(),
},
mocks: {
$apollo: {
mutate: jest.fn(),
},
},
}),
);
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
const findDaysHeaderSubItem = () => wrapper.findByTestId('day-item-sublabel');
const findDaysHeaderCurrentIndicator = () =>
wrapper.findByTestId('day-item-sublabel-current-indicator');
describe('template', () => {
it('renders component container element with class `item-sublabel`', () => {
expect(wrapper.classes()).toContain('item-sublabel');
});
it('renders sub item element with class `sublabel-value`', () => {
expect(wrapper.find('.sublabel-value').exists()).toBe(true);
});
it('renders element with class `current-day-indicator-header`', () => {
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 ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue'; import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue';
import WeeksHeaderItem from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue'; import WeeksHeaderItem from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue';
import DaysHeaderItem from 'ee/oncall_schedules/components/schedule/components/preset_days/days_header_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils'; import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import { getOncallSchedulesQueryResponse } from '../../mocks/apollo_mock'; import { getOncallSchedulesQueryResponse } from '../../mocks/apollo_mock';
...@@ -13,14 +14,12 @@ describe('TimelineSectionComponent', () => { ...@@ -13,14 +14,12 @@ describe('TimelineSectionComponent', () => {
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0]; getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
function createComponent({ function createComponent({
presetType = PRESET_TYPES.WEEKS, props = { presetType: PRESET_TYPES.WEEKS, timeframe: mockTimeframeWeeks },
timeframe = mockTimeframeWeeks,
} = {}) { } = {}) {
wrapper = shallowMount(ScheduleTimelineSection, { wrapper = shallowMount(ScheduleTimelineSection, {
propsData: { propsData: {
presetType,
timeframe,
schedule, schedule,
...props,
}, },
}); });
} }
...@@ -42,6 +41,11 @@ describe('TimelineSectionComponent', () => { ...@@ -42,6 +41,11 @@ describe('TimelineSectionComponent', () => {
}); });
it('renders weeks header items based on timeframe data', () => { it('renders weeks header items based on timeframe data', () => {
expect(wrapper.findAll(WeeksHeaderItem).length).toBe(mockTimeframeWeeks.length); expect(wrapper.findAllComponents(WeeksHeaderItem)).toHaveLength(mockTimeframeWeeks.length);
});
it('renders days header items based on timeframe data', () => {
createComponent({ props: { presetType: PRESET_TYPES.DAYS, timeframe: mockTimeframeWeeks } });
expect(wrapper.findAllComponents(DaysHeaderItem)).toHaveLength(1);
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import DaysScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/days_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-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, incrementDateByDays(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: '250px',
});
});
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 502px i.e. ((HOURS_IN_DAY - (HOURS_IN_DAY - overlapStartTime)) * CELL_WIDTH) + ASSIGNEE_SPACER(((24 - (24 - 9)) * 50)) + 2
* and width should be overlapping hours * CELL_WIDTH(12 * 50 + 50)
*/
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: '452px',
width: '650px',
});
});
});
describe('shift does not overlap inside the current time-frame or contains an invalid date', () => {
it.each`
reason | setTimeframeItem | 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 { setTimeframeItem, startsAt, endsAt } = data;
createComponent({
props: {
timeframeItem: setTimeframeItem,
shift: { ...shift, startsAt, endsAt },
},
});
expect(findRotationAssignee().exists()).toBe(false);
});
});
});
import { shallowMount } from '@vue/test-utils';
import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue';
import DaysScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/days_schedule_shift.vue';
import WeeksScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/weeks_schedule_shift.vue';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
import { incrementDateByDays } from 'ee/oncall_schedules/components/schedule/utils';
import mockRotations from '../../../../mocks/mock_rotation.json';
const timeframeItem = new Date(2021, 0, 13);
const timeframe = [timeframeItem, incrementDateByDays(timeframeItem, DAYS_IN_WEEK)];
describe('ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue', () => {
let wrapper;
function createComponent({ props = { presetType: PRESET_TYPES.WEEKS }, data = {} } = {}) {
wrapper = shallowMount(ScheduleShiftWrapper, {
propsData: {
timeframeItem,
timeframe,
rotation: mockRotations[0],
...props,
},
data() {
return {
shiftTimeUnitWidth: 0,
...data,
};
},
mocks: {
$apollo: {
queries: {
shiftTimeUnitWidth: 0,
},
},
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findDaysScheduleShifts = () => wrapper.findAllComponents(DaysScheduleShift);
const findWeeksScheduleShifts = () => wrapper.findAllComponents(WeeksScheduleShift);
describe('when the preset type is WEEKS', () => {
it('should render a selection of week grid shifts inside the rotation', () => {
expect(findWeeksScheduleShifts()).toHaveLength(2);
});
});
describe('when the preset type is DAYS', () => {
it('should render a selection of day grid shifts inside the rotation', () => {
createComponent({ props: { presetType: PRESET_TYPES.DAYS } });
expect(findDaysScheduleShifts()).toHaveLength(2);
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ScheduleShift from 'ee/oncall_schedules/components/schedule/components/schedule_shift.vue'; import WeeksScheduleShift from 'ee/oncall_schedules/components/schedule/components/shifts/components/weeks_schedule_shift.vue';
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 { incrementDateByDays } from 'ee/oncall_schedules/components/schedule/utils'; import { incrementDateByDays } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
...@@ -19,32 +19,20 @@ const CELL_WIDTH = 50; ...@@ -19,32 +19,20 @@ const CELL_WIDTH = 50;
const timeframeItem = new Date(2021, 0, 13); const timeframeItem = new Date(2021, 0, 13);
const timeframe = [timeframeItem, incrementDateByDays(timeframeItem, DAYS_IN_WEEK)]; const timeframe = [timeframeItem, incrementDateByDays(timeframeItem, DAYS_IN_WEEK)];
describe('ee/oncall_schedules/components/schedule/components/schedule_shift.vue', () => { describe('ee/oncall_schedules/components/schedule/components/shifts/components/weeks_schedule_shift.vue', () => {
let wrapper; let wrapper;
function createComponent({ props = {}, data = {} } = {}) { function createComponent({ props = {} } = {}) {
wrapper = shallowMount(ScheduleShift, { wrapper = shallowMount(WeeksScheduleShift, {
propsData: { propsData: {
shift, shift,
shiftIndex: 0, shiftIndex: 0,
timeframeItem, timeframeItem,
timeframe, timeframe,
presetType: PRESET_TYPES.WEEKS, presetType: PRESET_TYPES.WEEKS,
shiftTimeUnitWidth: CELL_WIDTH,
...props, ...props,
}, },
data() {
return {
shiftTimeUnitWidth: 0,
...data,
};
},
mocks: {
$apollo: {
queries: {
shiftTimeUnitWidth: 0,
},
},
},
}); });
} }
......
...@@ -843,7 +843,7 @@ describe('format24HourTimeStringFromInt', () => { ...@@ -843,7 +843,7 @@ describe('format24HourTimeStringFromInt', () => {
}); });
}); });
describe('getOverlappingDaysInPeriods', () => { describe('getOverlapDateInPeriods', () => {
const start = new Date(2021, 0, 11); const start = new Date(2021, 0, 11);
const end = new Date(2021, 0, 13); const end = new Date(2021, 0, 13);
...@@ -851,14 +851,15 @@ describe('getOverlappingDaysInPeriods', () => { ...@@ -851,14 +851,15 @@ describe('getOverlappingDaysInPeriods', () => {
const givenPeriodLeft = new Date(2021, 0, 11); const givenPeriodLeft = new Date(2021, 0, 11);
const givenPeriodRight = new Date(2021, 0, 14); 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', () => { 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( expect(
datetimeUtility.getOverlappingDaysInPeriods( datetimeUtility.getOverlapDateInPeriods(
{ start, end }, { start, end },
{ start: givenPeriodLeft, end: givenPeriodRight }, { start: givenPeriodLeft, end: givenPeriodRight },
), ),
).toEqual({ ).toEqual({
daysOverlap: 2, daysOverlap: 2,
hoursOverlap: 48,
overlapStartDate: givenPeriodLeft.getTime(), overlapStartDate: givenPeriodLeft.getTime(),
overlapEndDate: end.getTime(), overlapEndDate: end.getTime(),
}); });
...@@ -871,7 +872,7 @@ describe('getOverlappingDaysInPeriods', () => { ...@@ -871,7 +872,7 @@ describe('getOverlappingDaysInPeriods', () => {
it('returns an overlap object that contains a 0 value for days overlapping', () => { it('returns an overlap object that contains a 0 value for days overlapping', () => {
expect( expect(
datetimeUtility.getOverlappingDaysInPeriods( datetimeUtility.getOverlapDateInPeriods(
{ start, end }, { start, end },
{ start: givenPeriodLeft, end: givenPeriodRight }, { start: givenPeriodLeft, end: givenPeriodRight },
), ),
...@@ -886,13 +887,13 @@ describe('getOverlappingDaysInPeriods', () => { ...@@ -886,13 +887,13 @@ describe('getOverlappingDaysInPeriods', () => {
it('throws an exception when the left period contains an invalid date', () => { it('throws an exception when the left period contains an invalid date', () => {
expect(() => expect(() =>
datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start: startInvalid, end }), datetimeUtility.getOverlapDateInPeriods({ start, end }, { start: startInvalid, end }),
).toThrow(error); ).toThrow(error);
}); });
it('throws an exception when the right period contains an invalid date', () => { it('throws an exception when the right period contains an invalid date', () => {
expect(() => expect(() =>
datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start, end: endInvalid }), datetimeUtility.getOverlapDateInPeriods({ start, end }, { start, end: endInvalid }),
).toThrow(error); ).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