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

Add rotation assignee for schedule

Allow for GitLab Ui tokens
to be used inside the schedule
grid to draw assignees
parent 86a35180
......@@ -32,7 +32,7 @@
//// Copied from roadmaps.scss - adapted for on-call schedules
$header-item-height: 72px;
$item-height: 40px;
$details-cell-width: 150px;
$details-cell-width: 180px;
$timeline-cell-height: 32px;
$timeline-cell-width: 180px;
$border-style: 1px solid var(--gray-100, $gray-100);
......@@ -98,7 +98,7 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
.item-label {
@include gl-py-4;
@include gl-pl-7;
@include gl-pl-4;
border-right: $border-style;
border-bottom: $border-style;
}
......@@ -147,7 +147,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
.timeline-cell {
@include float-left;
height: $item-height;
border-bottom: $border-style;
}
.details-cell {
......@@ -181,3 +180,10 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
transform: translateX(-50%);
}
}
.gl-token {
.gl-avatar-labeled-label {
@include gl-text-white;
@include gl-font-weight-normal;
}
}
......@@ -7,19 +7,19 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
import { s__, __ } from '~/locale';
import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue';
import DeleteScheduleModal from './delete_schedule_modal.vue';
import EditScheduleModal from './edit_schedule_modal.vue';
import AddRotationModal from './rotations/add_rotation_modal.vue';
import AddRotationModal from './rotations/components/add_rotation_modal.vue';
import { getTimeframeForWeeksView } from './schedule/utils';
import { PRESET_TYPES } from './schedule/constants';
import { getFormattedTimezone } from '../utils/common_utils';
import { PRESET_TYPES } from '../constants';
import RotationsListSection from './schedule/components/rotations_list_section.vue';
export const i18n = {
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'),
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{timezone}'),
editScheduleLabel: s__('OnCallSchedules|Edit schedule'),
deleteScheduleLabel: s__('OnCallSchedules|Delete schedule'),
rotationTitle: s__('OnCallSchedules|Rotations'),
......@@ -64,23 +64,28 @@ export default {
},
},
computed: {
tzLong() {
offset() {
const selectedTz = this.timezones.find(tz => tz.identifier === this.schedule.timezone);
return getFormattedTimezone(selectedTz);
return __(`(UTC ${selectedTz.formatted_offset})`);
},
timeframe() {
return getTimeframeForWeeksView();
},
scheduleRange() {
const range = { start: this.timeframe[0], end: this.timeframe[this.timeframe.length - 1] };
return `${formatDate(range.start, 'mmmm d')} - ${formatDate(range.end, 'mmmm d, yyyy')}`;
},
},
};
</script>
<template>
<div>
<gl-card>
<gl-card class="gl-mt-5" header-class="gl-py-3">
<template #header>
<div
class="gl-display-flex gl-justify-content-space-between gl-m-0"
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-m-0"
data-testid="scheduleHeader"
>
<span class="gl-font-weight-bold gl-font-lg">{{ schedule.name }}</span>
......@@ -102,12 +107,19 @@ export default {
</gl-button-group>
</div>
</template>
<p class="gl-text-gray-500 gl-mb-5" data-testid="scheduleBody">
<p class="gl-text-gray-500 gl-mb-3" data-testid="scheduleBody">
<gl-sprintf :message="$options.i18n.scheduleForTz">
<template #tzShort>{{ schedule.timezone }}</template>
<template #timezone>{{ schedule.timezone }}</template>
</gl-sprintf>
| {{ tzLong }}
| {{ offset }}
</p>
<div class="gl-w-full gl-display-flex gl-align-items-center gl-pb-3">
<gl-button-group>
<gl-button icon="chevron-left" />
<gl-button icon="chevron-right" />
</gl-button-group>
<p class="gl-ml-3 gl-mb-0">{{ scheduleRange }}</p>
</div>
<gl-card header-class="gl-bg-transparent">
<template #header>
......@@ -134,6 +146,6 @@ export default {
</gl-card>
<delete-schedule-modal :schedule="schedule" :modal-id="$options.deleteScheduleModalId" />
<edit-schedule-modal :schedule="schedule" :modal-id="$options.editScheduleModalId" />
<add-rotation-modal :modal-id="$options.addRotationModalId" />
<add-rotation-modal :schedule="schedule" :modal-id="$options.addRotationModalId" />
</div>
</template>
<script>
import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import mockRotations from '../../../../../spec/frontend/oncall_schedule/mocks/mock_rotation.json';
import * as Sentry from '~/sentry/wrapper';
import AddScheduleModal from './add_schedule_modal.vue';
import OncallSchedule from './oncall_schedule.vue';
......@@ -25,6 +26,7 @@ export const i18n = {
};
export default {
mockRotations,
i18n,
addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath', 'projectPath'],
......@@ -86,7 +88,7 @@ export default {
>
{{ $options.i18n.successNotification.description }}
</gl-alert>
<oncall-schedule :schedule="schedule" />
<oncall-schedule :schedule="schedule" :rotations="$options.mockRotations" />
</template>
<gl-empty-state
......
......@@ -14,12 +14,12 @@ import {
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import createOncallScheduleRotationMutation from '../../graphql/create_oncall_schedule_rotation.mutation.graphql';
import createOncallScheduleRotationMutation from '../../../graphql/create_oncall_schedule_rotation.mutation.graphql';
import {
LENGTH_ENUM,
CHEVRON_SKIPPING_SHADE_ENUM,
CHEVRON_SKIPPING_PALETTE_ENUM,
} from '../../constants';
} from '../../../constants';
export default {
i18n: {
......@@ -65,6 +65,10 @@ export default {
type: String,
required: true,
},
schedule: {
type: Object,
required: true,
},
},
apollo: {
participants: {
......@@ -78,7 +82,6 @@ export default {
return nodes;
},
error(error) {
this.showErrorAlert = true;
this.error = error;
},
},
......@@ -100,8 +103,12 @@ export default {
time: 0,
},
},
showErrorAlert: false,
error: '',
error: null,
validationState: {
name: true,
participants: true,
startsOn: true,
},
};
},
computed: {
......@@ -116,15 +123,6 @@ export default {
},
};
},
rotationNameIsValid() {
return this.form.name !== '';
},
rotationParticipantsAreValid() {
return this.form.participants.length > 0;
},
rotationStartsOnIsValid() {
return this.form.startsOn.date !== null || this.form.startsOn.date !== undefined;
},
noResults() {
return this.participants.length === 0;
},
......@@ -150,7 +148,6 @@ export default {
})
.catch(error => {
this.error = error;
this.showErrorAlert = true;
})
.finally(() => {
this.loading = false;
......@@ -168,6 +165,15 @@ export default {
setRotationStartsOnTime(time) {
this.form.startsOn.time = time;
},
validateForm(key) {
if (key === 'name') {
this.validationState.name = this.form.name !== '';
} else if (key === 'participants') {
this.validationState.participants = this.form.participants.length > 0;
} else if (key === 'startsOn') {
this.validationState.startsOn = this.form.startsOn.date !== null;
}
},
},
};
</script>
......@@ -182,7 +188,7 @@ export default {
:action-cancel="actionsProps.cancel"
@primary="createRotation"
>
<gl-alert v-if="showErrorAlert" variant="danger" @dismiss="showErrorAlert = false">
<gl-alert v-if="error" variant="danger" @dismiss="error = null">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<gl-form class="w-75 gl-xs-w-full!" @submit.prevent="createRotation">
......@@ -191,9 +197,9 @@ export default {
label-size="sm"
label-for="rotation-name"
:invalid-feedback="$options.i18n.fields.name.error"
:state="rotationNameIsValid"
:state="validationState.name"
>
<gl-form-input id="rotation-name" v-model="form.name" />
<gl-form-input id="rotation-name" v-model="form.name" @blur.native="validateForm('name')" />
</gl-form-group>
<gl-form-group
......@@ -201,7 +207,7 @@ export default {
label-size="sm"
label-for="rotation-participants"
:invalid-feedback="$options.i18n.fields.participants.error"
:state="rotationParticipantsAreValid"
:state="validationState.participants"
>
<gl-token-selector
v-model="form.participants"
......@@ -209,6 +215,7 @@ export default {
:loading="this.$apollo.queries.participants.loading"
:container-class="'gl-h-13! gl-overflow-y-auto'"
@text-input="filterParticipants"
@blur="validateForm('participants')"
>
<template #token-content="{ token }">
<gl-avatar v-if="token.avatarUrl" :src="token.avatarUrl" :size="16" />
......@@ -257,10 +264,14 @@ export default {
label-size="sm"
label-for="rotation-time"
:invalid-feedback="$options.i18n.fields.startsOn.error"
:state="rotationStartsOnIsValid"
:state="validationState.startsOn"
>
<div class="gl-display-flex gl-align-items-center">
<gl-datepicker v-model="form.startsOn.date" class="gl-mr-3" />
<gl-datepicker
v-model="form.startsOn.date"
class="gl-mr-3"
@close="validateForm('startsOn')"
/>
<span> {{ __('at') }} </span>
<gl-dropdown
id="rotation-time"
......@@ -277,8 +288,7 @@ export default {
<span class="gl-white-space-nowrap"> {{ formatTime(n) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<!-- TODO: // Replace with actual timezone following coming work -->
<span class="gl-pl-5"> {{ __('PST') }} </span>
<span class="gl-pl-5"> {{ schedule.timezone }} </span>
</div>
</gl-form-group>
</gl-form>
......
<script>
import { GlToken, GlAvatarLabeled, GlPopover } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { assigneeScheduleDateStart } from '../../../utils/common_utils';
export default {
components: {
GlToken,
GlAvatarLabeled,
GlPopover,
},
props: {
assigneeIndex: {
type: Number,
required: true,
},
assignee: {
type: Object,
required: true,
},
rotationLength: {
type: Number,
required: true,
},
rotationStartsAt: {
type: String,
required: true,
},
},
computed: {
chevronClass() {
return `gl-bg-data-viz-${this.assignee.colorPalette}-${this.assignee.colorWeight}`;
},
startsAt() {
const startsAt = assigneeScheduleDateStart(
new Date(this.rotationStartsAt),
this.rotationLength * 7 * this.assigneeIndex,
).toLocaleString();
return sprintf(__('Starts at %{startsAt}'), { startsAt });
},
endsAt() {
const endsAt = assigneeScheduleDateStart(
new Date(this.rotationStartsAt),
this.rotationLength * 7 * this.assigneeIndex + this.rotationLength * 7,
).toLocaleString();
return sprintf(__('Ends at %{endsAt}'), { endsAt });
},
},
};
</script>
<template>
<div class="gl-w-full gl-mt-3 gl-px-3">
<gl-token
:id="assignee.user.id"
class="gl-w-full gl-align-items-center"
:class="chevronClass"
:view-only="true"
>
<gl-avatar-labeled
shape="circle"
:size="16"
:src="assignee.user.avatarUrl"
:label="assignee.user.username"
:title="assignee.user.username"
/>
</gl-token>
<gl-popover
:target="assignee.user.id"
:title="assignee.user.username"
triggers="hover"
placement="left"
>
<p class="gl-m-0" data-testid="rotation-assignee-starts-at">{{ startsAt }}</p>
<p class="gl-m-0" data-testid="rotation-assignee-ends-at">{{ endsAt }}</p>
</gl-popover>
</div>
</template>
<script>
import CommonMixin from '../mixins/common_mixin';
import CommonMixin from '../../../mixins/common_mixin';
export default {
mixins: [CommonMixin],
......
<script>
import { monthInWords } from '~/lib/utils/datetime_utility';
import WeeksHeaderSubItem from './weeks_header_sub_item.vue';
import CommonMixin from '../../mixins/common_mixin';
import CommonMixin from '../../../../mixins/common_mixin';
export default {
components: {
......@@ -34,10 +34,7 @@ export default {
const timeframeItemDate = this.timeframeItem.getDate();
if (this.timeframeIndex === 0 || (timeframeItemMonth === 0 && timeframeItemDate <= 7)) {
return `${this.timeframeItem.getFullYear()} ${monthInWords(
this.timeframeItem,
true,
)} ${timeframeItemDate}`;
return `${monthInWords(this.timeframeItem, true)} ${timeframeItemDate}`;
}
return `${monthInWords(this.timeframeItem, true)} ${timeframeItemDate}`;
......
<script>
import CommonMixin from '../../mixins/common_mixin';
import CommonMixin from '../../../../mixins/common_mixin';
export default {
mixins: [CommonMixin],
......
<script>
import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import CurrentDayIndicator from './current_day_indicator.vue';
import RotationAssignee from '../../rotations/components/rotation_assignee.vue';
export const i18n = {
editRotationLabel: s__('OnCallSchedules|Edit rotation'),
deleteRotationLabel: s__('OnCallSchedules|Delete rotation'),
};
export default {
i18n,
components: {
GlButtonGroup,
GlButton,
CurrentDayIndicator,
RotationAssignee,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
presetType: {
......@@ -24,8 +39,32 @@ export default {
<template>
<div class="list-section">
<div class="list-item list-item-empty clearfix">
<span class="details-cell"></span>
<div
v-for="rotation in rotations"
:key="rotation.id"
class="list-item list-item-empty clearfix"
>
<span
class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3"
>
<span class="gl-str-truncated">{{ rotation.name }}</span>
<gl-button-group class="gl-px-2">
<gl-button
v-gl-tooltip
category="tertiary"
:title="$options.i18n.editRotationLabel"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
/>
<gl-button
v-gl-tooltip
category="tertiary"
:title="$options.i18n.deleteRotationLabel"
icon="remove"
:aria-label="$options.i18n.deleteRotationLabel"
/>
</gl-button-group>
</span>
<span
v-for="(timeframeItem, index) in timeframe"
:key="index"
......@@ -33,6 +72,12 @@ export default {
data-testid="timelineCell"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<rotation-assignee
:assignee="rotation.participants.nodes[index]"
:assignee-index="index"
:rotation-length="rotation.length"
:rotation-starts-at="rotation.startsAt"
/>
</span>
</div>
</div>
......
export const DAYS_IN_WEEK = 7;
export const PRESET_TYPES = {
WEEKS: 'WEEKS',
};
export const PRESET_DEFAULTS = {
WEEKS: {
TIMEFRAME_LENGTH: 2,
},
};
import { newDate } from '~/lib/utils/datetime_utility';
import { PRESET_DEFAULTS, DAYS_IN_WEEK } from './constants';
import { PRESET_DEFAULTS, DAYS_IN_WEEK } from '../../constants';
/**
* This method returns array of Dates representing 2-weeks timeframe based on provided initialDate
......
......@@ -7,3 +7,15 @@ export const LENGTH_ENUM = {
export const CHEVRON_SKIPPING_SHADE_ENUM = ['500', '600', '700', '800', '900', '950'];
export const CHEVRON_SKIPPING_PALETTE_ENUM = ['blue', 'orange', 'aqua', 'green', 'magenta'];
export const DAYS_IN_WEEK = 7;
export const PRESET_TYPES = {
WEEKS: 'WEEKS',
};
export const PRESET_DEFAULTS = {
WEEKS: {
TIMEFRAME_LENGTH: 2,
},
};
import { sprintf, __ } from '~/locale';
import { getDateInFuture } from '~/lib/utils/datetime_utility';
/**
* Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska
......@@ -16,3 +17,16 @@ export const getFormattedTimezone = tz => {
timezone: `${tz.abbr} ${tz.name}`,
});
};
/**
* Returns formatted date of the rotation assignee
* based on the rotation start time and length
*
* @param {Date} startDate
* @param {Number} daysToAdd
*
* @returns {Date}
*/
export const assigneeScheduleDateStart = (startDate, daysToAdd) => {
return getDateInFuture(startDate, daysToAdd);
};
[{
"id": "gid://gitlab/IncidentManagement::OncallRotation/2",
"name": "Rotation 242",
"startsAt": "2020-12-09T09:00:53Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
"nodes": [
{
"user": {
"id": "gid://gitlab/User/1",
"username": "root",
"avatarUrl": "/url"
},
"colorWeight": "500",
"colorPalette": "blue"
},
{
"user": {
"id": "gid://gitlab/User/2",
"username": "david",
"avatarUrl": "/url"
},
"colorWeight": "500",
"colorPalette": "magenta"
}
]
}
},
{
"id": "gid://gitlab/IncidentManagement::OncallRotation/3",
"name": "Rotation 244",
"startsAt": "2020-12-16T09:00:53Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
"nodes": [
{
"user": {
"id": "gid://gitlab/User/3",
"username": "root 2",
"avatarUrl": "/url"
},
"colorWeight": "500",
"colorPalette": "orange"
},
{
"user": {
"id": "gid://gitlab/User/4",
"username": "david 2",
"avatarUrl": "/url"
},
"colorWeight": "500",
"colorPalette": "aqua"
}
]
}
}]
......@@ -5,7 +5,7 @@ import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/com
import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue';
import * as utils from 'ee/oncall_schedules/components/schedule/utils';
import * as commonUtils from 'ee/oncall_schedules/utils/common_utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockTimezones from './mocks/mockTimezones.json';
......@@ -63,11 +63,11 @@ describe('On-call schedule', () => {
});
it('shows timezone info', () => {
const shortTz = i18n.scheduleForTz.replace('%{tzShort}', lastTz.identifier);
const longTz = formattedTimezone;
const timezone = i18n.scheduleForTz.replace('%{timezone}', lastTz.identifier);
const offset = `(UTC ${lastTz.formatted_offset})`;
const description = findSchedule().text();
expect(description).toContain(shortTz);
expect(description).toContain(longTz);
expect(description).toContain(timezone);
expect(description).toContain(offset);
});
it('renders rotations header', () => {
......
......@@ -20,6 +20,7 @@ exports[`AddRotationModal renders rotation modal layout 1`] = `
label="Name"
label-for="rotation-name"
label-size="sm"
state="true"
>
<gl-form-input-stub
id="rotation-name"
......@@ -32,6 +33,7 @@ exports[`AddRotationModal renders rotation modal layout 1`] = `
label="Participants"
label-for="rotation-participants"
label-size="sm"
state="true"
>
<gl-token-selector-stub
autocomplete="off"
......@@ -509,7 +511,9 @@ exports[`AddRotationModal renders rotation modal layout 1`] = `
<span
class="gl-pl-5"
>
PST
{
"identifier": "Pacific/Honolulu"
}
</span>
</div>
</gl-form-group-stub>
......
......@@ -4,11 +4,13 @@ import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { GlDropdownItem, GlModal, GlAlert, GlTokenSelector } from '@gitlab/ui';
import { addRotationModalId } from 'ee/oncall_schedules/components/oncall_schedule';
import AddRotationModal from 'ee/oncall_schedules/components/rotations/add_rotation_modal.vue';
import AddRotationModal from 'ee/oncall_schedules/components/rotations/components/add_rotation_modal.vue';
// import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/create_oncall_schedule_rotation.mutation.graphql';
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import { participants } from '../mocks/apollo_mock';
import { getOncallSchedulesQueryResponse, participants } from '../../mocks/apollo_mock';
const schedule =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
const localVue = createLocalVue();
const projectPath = 'group/project';
const mutate = jest.fn();
......@@ -36,6 +38,7 @@ describe('AddRotationModal', () => {
},
propsData: {
modalId: addRotationModalId,
schedule,
...props,
},
provide: {
......@@ -62,6 +65,7 @@ describe('AddRotationModal', () => {
localVue,
propsData: {
modalId: addRotationModalId,
schedule,
},
apolloProvider: fakeApollo,
data() {
......
import { shallowMount } from '@vue/test-utils';
import { GlToken, GlAvatarLabeled, GlPopover } from '@gitlab/ui';
import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockRotations from '../../mocks/mock_rotation.json';
describe('RotationAssignee', () => {
let wrapper;
const assignee = mockRotations[0].participants.nodes[1];
const findToken = () => wrapper.find(GlToken);
const findAvatar = () => wrapper.find(GlAvatarLabeled);
const findPopOver = () => wrapper.find(GlPopover);
const findStartsAt = () => wrapper.findByTestId('rotation-assignee-starts-at');
const findEndsAt = () => wrapper.findByTestId('rotation-assignee-ends-at');
function mountComponent() {
wrapper = extendedWrapper(
shallowMount(RotationAssignee, {
propsData: {
assignee,
assigneeIndex: 1,
rotationLength: mockRotations[0].length,
rotationStartsAt: mockRotations[0].startsAt,
},
}),
);
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('rotation assignee token', () => {
it('should render an assignee name', () => {
expect(findAvatar().attributes('label')).toBe(assignee.user.username);
});
it('should render an assignee avatar', () => {
expect(findAvatar().attributes('src')).toBe(assignee.user.avatarUrl);
});
it('should render an assignee color based on the chevron skipping color pallette', () => {
const token = findToken();
expect(token.classes()).toContain(
`gl-bg-data-viz-${assignee.colorPalette}-${assignee.colorWeight}`,
);
});
it('should render an assignee schedule and rotation information in a popover', () => {
expect(findPopOver().attributes('target')).toBe(assignee.user.id);
// starts at the beginning of the rotation time
expect(findStartsAt().text()).toContain('12/16/2020');
// ends at the calculated length of the rotation for this user: rotation length * which user index assignee is at
expect(findEndsAt().text()).toContain('12/23/2020');
});
});
});
......@@ -8,8 +8,38 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
class="list-item list-item-empty clearfix"
>
<span
class="details-cell"
/>
class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3"
>
<span
class="gl-str-truncated"
>
Rotation 242
</span>
<gl-button-group-stub
class="gl-px-2"
>
<gl-button-stub
aria-label="Edit rotation"
buttontextclasses=""
category="tertiary"
icon="pencil"
size="medium"
title="Edit rotation"
variant="default"
/>
<gl-button-stub
aria-label="Delete rotation"
buttontextclasses=""
category="tertiary"
icon="remove"
size="medium"
title="Delete rotation"
variant="default"
/>
</gl-button-group-stub>
</span>
<span
class="timeline-cell"
......@@ -19,6 +49,13 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
presettype="WEEKS"
timeframeitem="Mon Jan 01 2018 00:00:00 GMT+0000 (Greenwich Mean Time)"
/>
<rotation-assignee-stub
assignee="[object Object]"
assigneeindex="0"
rotationlength="1"
rotationstartsat="2020-12-09T09:00:53Z"
/>
</span>
<span
class="timeline-cell"
......@@ -28,6 +65,13 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
presettype="WEEKS"
timeframeitem="Mon Jan 08 2018 00:00:00 GMT+0000 (Greenwich Mean Time)"
/>
<rotation-assignee-stub
assignee="[object Object]"
assigneeindex="1"
rotationlength="1"
rotationstartsat="2020-12-09T09:00:53Z"
/>
</span>
</div>
</div>
......
import { shallowMount } from '@vue/test-utils';
import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/components/schedule/constants';
import { useFakeDate } from 'helpers/fake_date';
import { PRESET_TYPES, DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
describe('CurrentDayIndicator', () => {
let wrapper;
......
......@@ -48,7 +48,7 @@ describe('WeeksHeaderItemComponent', () => {
describe('timelineHeaderLabel', () => {
it('returns string containing Year, Month and Date for the first timeframe item in the entire timeframe', () => {
expect(findHeaderLabel().text()).toBe('2018 Jan 1');
expect(findHeaderLabel().text()).toBe('Jan 1');
});
it('returns string containing Year, Month and Date for timeframe item that is the first week of the year', () => {
......@@ -57,7 +57,7 @@ describe('WeeksHeaderItemComponent', () => {
timeframeItem: new Date(2019, 0, 6),
});
expect(findHeaderLabel().text()).toBe('2019 Jan 6');
expect(findHeaderLabel().text()).toBe('Jan 6');
});
it('returns string containing only Month and Date when timeframe item is somewhere in the middle of the timeframe', () => {
......
......@@ -2,8 +2,10 @@ import { shallowMount } from '@vue/test-utils';
import { GlCard } from '@gitlab/ui';
import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue';
import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue';
import RotationsAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import mockRotations from '../../mocks/mock_rotation.json';
describe('RotationsListSectionComponent', () => {
let wrapper;
......@@ -18,7 +20,7 @@ describe('RotationsListSectionComponent', () => {
propsData: {
presetType,
timeframe,
rotations: [],
rotations: [mockRotations[0]],
},
stubs: {
GlCard,
......@@ -38,6 +40,7 @@ describe('RotationsListSectionComponent', () => {
});
const findTimelineCells = () => wrapper.findAll('[data-testid="timelineCell"]');
const findRotationAssignees = () => wrapper.findAll(RotationsAssignee);
it('renders component layout', () => {
expect(wrapper.element).toMatchSnapshot();
......@@ -55,4 +58,13 @@ describe('RotationsListSectionComponent', () => {
.exists(),
).toBe(true);
});
it('render the correct amount of rotation assignees with their name, avatar and color', () => {
expect(findRotationAssignees()).toHaveLength(2);
expect(
findRotationAssignees()
.at(0)
.props().assignee.user,
).toEqual(mockRotations[0].participants.nodes[0].user);
});
});
......@@ -3,7 +3,7 @@ import { GlCard } from '@gitlab/ui';
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 { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
describe('TimelineSectionComponent', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import CommonMixin from 'ee/oncall_schedules/components/schedule/mixins/common_mixin';
import { DAYS_IN_WEEK } from 'ee/oncall_schedules/components/schedule/constants';
import CommonMixin from 'ee/oncall_schedules/mixins/common_mixin';
import { useFakeDate } from 'helpers/fake_date';
import { DAYS_IN_WEEK } from 'ee/oncall_schedules/constants';
describe('Schedule Common Mixins', () => {
// January 3rd, 2018
......
......@@ -10581,6 +10581,9 @@ msgstr ""
msgid "End Time"
msgstr ""
msgid "Ends at %{endsAt}"
msgstr ""
msgid "Ends at (UTC)"
msgstr ""
......@@ -19352,9 +19355,15 @@ msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr ""
msgid "OnCallSchedules|Delete rotation"
msgstr ""
msgid "OnCallSchedules|Delete schedule"
msgstr ""
msgid "OnCallSchedules|Edit rotation"
msgstr ""
msgid "OnCallSchedules|Edit schedule"
msgstr ""
......@@ -19370,7 +19379,7 @@ msgstr ""
msgid "OnCallSchedules|On-call schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule for the %{tzShort}"
msgid "OnCallSchedules|On-call schedule for the %{timezone}"
msgstr ""
msgid "OnCallSchedules|Rotation length"
......@@ -19723,9 +19732,6 @@ msgstr ""
msgid "Owner"
msgstr ""
msgid "PST"
msgstr ""
msgid "Package Registry"
msgstr ""
......@@ -26402,6 +26408,9 @@ msgstr ""
msgid "Starts %{startsIn}"
msgstr ""
msgid "Starts at %{startsAt}"
msgstr ""
msgid "Starts at (UTC)"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment