Skip to content
Snippets Groups Projects
Commit 86e90425 authored by Miguel Rincon's avatar Miguel Rincon
Browse files

Merge branch '344950-color-rotation-schedules-in-fe' into 'master'

Resolve "Visual bugs in the "add a rotation" modal"

See merge request !90497
parents 953541f2 19906d6a
No related branches found
No related tags found
2 merge requests!96059Draft: Add GraphQL query for deployment details,!90497Resolve "Visual bugs in the "add a rotation" modal"
Pipeline #616846172 passed
Showing
with 471 additions and 300 deletions
......@@ -19,6 +19,7 @@ import {
CHEVRON_SKIPPING_PALETTE_ENUM,
} from 'ee/oncall_schedules/constants';
import { format24HourTimeStringFromInt } from '~/lib/utils/datetime_utility';
import { formatParticipantsForTokenSelector } from 'ee/oncall_schedules/utils/common_utils';
import { s__, __ } from '~/locale';
export const i18n = {
......@@ -97,6 +98,11 @@ export default {
default: () => ({}),
},
},
computed: {
formParticipantsWithTokenStyles() {
return formatParticipantsForTokenSelector(this.form.participants);
},
},
methods: {
format24HourTimeStringFromInt,
},
......@@ -128,7 +134,7 @@ export default {
:state="validationState.participants"
>
<gl-token-selector
:selected-tokens="form.participants"
:selected-tokens="formParticipantsWithTokenStyles"
:dropdown-items="participants"
:loading="isLoading"
container-class="gl-h-13! gl-overflow-y-auto"
......
......@@ -10,8 +10,6 @@ import {
isNameFieldValid,
parseHour,
parseRotationDate,
setParticipantsColors,
getUserTokenStyles,
getParticipantsForSave,
} from 'ee/oncall_schedules/utils/common_utils';
import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql';
......@@ -199,9 +197,6 @@ export default {
}
return false;
},
participantsWithTokenStylesData() {
return setParticipantsColors(this.participants, this.rotation?.participants?.nodes);
},
},
methods: {
createRotation() {
......@@ -322,10 +317,7 @@ export default {
this.form.name = this.rotation.name;
const participants =
this.rotation?.participants?.nodes?.map(({ user, colorWeight, colorPalette }) =>
getUserTokenStyles({ ...user, colorWeight, colorPalette }),
) ?? [];
const participants = this.rotation?.participants?.nodes.map(({ user }) => user) ?? [];
this.form.participants = participants;
this.form.rotationLength = {
......@@ -372,7 +364,7 @@ export default {
:validation-state="validationState"
:form="form"
:schedule="schedule"
:participants="participantsWithTokenStylesData"
:participants="participants"
:is-loading="isLoading"
@update-rotation-form="updateRotationForm"
@filter-participants="filterParticipants"
......
<script>
import { GlAvatar, GlPopover } from '@gitlab/ui';
import * as cssVariables from '@gitlab/ui/scss_to_js/scss_variables';
import { uniqueId, startCase } from 'lodash';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { uniqueId } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility';
import { truncate } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import { LIGHT_TO_DARK_MODE_SHADE_MAPPING } from '../../../constants';
export const SHIFT_WIDTHS = {
md: 100,
......@@ -28,23 +25,33 @@ export default {
type: Object,
required: true,
},
rotationAssigneeStartsAt: {
startsAt: {
type: String,
required: true,
},
rotationAssigneeEndsAt: {
endsAt: {
type: String,
required: true,
},
rotationAssigneeStyle: {
containerStyle: {
type: Object,
required: true,
},
shiftWidth: {
type: Number,
color: {
type: Object,
required: true,
},
},
data() {
const { colorWeight, backgroundStyle, textClass } = this.color;
return {
colorWeight,
backgroundStyle,
textClass,
shiftWidth: parseInt(this.containerStyle.width, 10),
};
},
computed: {
assigneeName() {
if (this.shiftWidth <= SHIFT_WIDTHS.md) {
......@@ -53,25 +60,9 @@ export default {
return this.assignee.user.username;
},
colorWeight() {
const { colorWeight } = this.assignee;
return darkModeEnabled() ? LIGHT_TO_DARK_MODE_SHADE_MAPPING[colorWeight] : colorWeight;
},
chevronBackground() {
const { colorPalette } = this.assignee;
const bgColor = `dataViz${startCase(colorPalette)}${this.colorWeight}`;
return cssVariables[bgColor];
},
textClass() {
if (darkModeEnabled()) {
return this.colorWeight < 500 ? 'gl-text-white' : 'gl-text-gray-900';
}
return 'gl-text-white';
},
endsAt() {
endsAtString() {
return sprintf(__('Ends: %{endsAt}'), {
endsAt: `${formatDate(this.rotationAssigneeEndsAt, TIME_DATE_FORMAT)}`,
endsAt: `${formatDate(this.endsAt, TIME_DATE_FORMAT)}`,
});
},
rotationAssigneeUniqueID() {
......@@ -83,9 +74,9 @@ export default {
hasRotationMobileViewText() {
return this.shiftWidth <= SHIFT_WIDTHS.sm;
},
startsAt() {
startsAtString() {
return sprintf(__('Starts: %{startsAt}'), {
startsAt: `${formatDate(this.rotationAssigneeStartsAt, TIME_DATE_FORMAT)}`,
startsAt: `${formatDate(this.startsAt, TIME_DATE_FORMAT)}`,
});
},
},
......@@ -93,11 +84,11 @@ export default {
</script>
<template>
<div class="gl-absolute gl-h-7 gl-mt-3 gl-pr-1" :style="rotationAssigneeStyle">
<div class="gl-absolute gl-h-7 gl-mt-3 gl-pr-1" :style="containerStyle">
<div
:id="rotationAssigneeUniqueID"
class="gl-h-6"
:style="{ backgroundColor: chevronBackground }"
:style="backgroundStyle"
:class="$options.ROTATION_CENTER_CLASS"
data-testid="rotation-assignee"
>
......@@ -113,10 +104,10 @@ export default {
</div>
<gl-popover :target="rotationAssigneeUniqueID" :title="assignee.user.username" placement="top">
<p class="gl-m-0" data-testid="rotation-assignee-starts-at">
{{ startsAt }}
{{ startsAtString }}
</p>
<p class="gl-m-0" data-testid="rotation-assignee-ends-at">
{{ endsAt }}
{{ endsAtString }}
</p>
</gl-popover>
</div>
......
<script>
import { SHIFT_WIDTH_CALCULATION_DELAY } from 'ee/oncall_schedules/constants';
import getTimelineWidthQuery from 'ee/oncall_schedules/graphql/queries/get_timeline_width.query.graphql';
import { getParticipantColor } from 'ee/oncall_schedules/utils/common_utils';
import ShiftItem from './shift_item.vue';
export default {
......@@ -36,6 +37,21 @@ export default {
shiftsToRender() {
return Object.freeze(this.rotation.shifts.nodes);
},
participantIdsWithIndex() {
const participantIdsWithIndex = {};
this.rotation.participants.nodes.forEach(({ id }, index) => {
participantIdsWithIndex[id] = index;
});
return participantIdsWithIndex;
},
},
methods: {
getShiftColor(participantId) {
const participantIndex = this.participantIdsWithIndex[participantId];
return getParticipantColor(participantIndex);
},
},
};
</script>
......@@ -49,6 +65,7 @@ export default {
:preset-type="presetType"
:timeframe="timeframe"
:timeline-width="timelineWidth"
:shift-color="getShiftColor(shift.participant.id)"
/>
</div>
</template>
<script>
import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { getPixelOffset, getPixelWidth } from './shift_utils';
import { getShiftContainerStyles } from './shift_utils';
export default {
components: {
......@@ -23,36 +23,15 @@ export default {
type: Number,
required: true,
},
shiftColor: {
type: Object,
required: true,
},
},
computed: {
shiftStyles() {
containerStyle() {
const { timeframe, presetType, timelineWidth, shift } = this;
return {
left: getPixelOffset({
timeframe,
shift,
timelineWidth,
presetType,
}),
width: Math.round(
getPixelWidth({
shift,
timelineWidth,
presetType,
shiftDLSOffset:
new Date(shift.startsAt).getTimezoneOffset() -
new Date(shift.endsAt).getTimezoneOffset(),
}),
),
};
},
rotationAssigneeStyle() {
const { left, width } = this.shiftStyles;
return {
left: `${left}px`,
width: `${width}px`,
};
return getShiftContainerStyles({ timeframe, presetType, timelineWidth, shift });
},
},
};
......@@ -61,9 +40,9 @@ export default {
<template>
<rotation-assignee
:assignee="shift.participant"
:rotation-assignee-style="rotationAssigneeStyle"
:rotation-assignee-starts-at="shift.startsAt"
:rotation-assignee-ends-at="shift.endsAt"
:shift-width="shiftStyles.width"
:container-style="containerStyle"
:starts-at="shift.startsAt"
:ends-at="shift.endsAt"
:color="shiftColor"
/>
</template>
......@@ -82,10 +82,31 @@ export const getPixelOffset = ({ timeframe, shift, timelineWidth, presetType })
* @param {Object} shift, timelineWidth, presetType, shiftDLSOffset
* @return {Number} width in pixels
*/
export const getPixelWidth = ({ shift, timelineWidth, presetType, shiftDLSOffset }) => {
const totalTime = getTotalTime(presetType);
const getPixelWidth = ({ shift, timelineWidth, presetType }) => {
const displayRangeTotal = getTotalTime(presetType);
const durationMillis = getDuration(shift);
const DLS = milliseconds({ m: shiftDLSOffset });
const shiftDayListSavingsOffset =
new Date(shift.startsAt).getTimezoneOffset() - new Date(shift.endsAt).getTimezoneOffset();
const DLS = milliseconds({ m: shiftDayListSavingsOffset });
// shift width (px) = shift time (ms) * total width (px) / total time (ms)
return ((durationMillis + DLS) * timelineWidth) / totalTime;
return Math.round(((durationMillis + DLS) * timelineWidth) / displayRangeTotal);
};
export const getShiftContainerStyles = ({ timeframe, presetType, timelineWidth, shift }) => {
const left = getPixelOffset({
timeframe,
shift,
timelineWidth,
presetType,
});
const width = getPixelWidth({
shift,
timelineWidth,
presetType,
});
return {
left: `${left}px`,
width: `${width}px`,
};
};
import { gray500 } from '@gitlab/ui/scss_to_js/scss_variables';
export const LENGTH_ENUM = {
hours: 'HOURS',
days: 'DAYS',
......@@ -48,3 +50,10 @@ export const SHIFT_WIDTH_CALCULATION_DELAY = 250;
export const oneHourOffsetDayView = 100 / HOURS_IN_DAY;
export const oneDayOffsetWeekView = 100 / DAYS_IN_WEEK;
export const oneHourOffsetWeekView = oneDayOffsetWeekView / HOURS_IN_DAY;
export const NON_ACTIVE_PARTICIPANT_STYLE = {
colorWeight: '500',
colorPalette: 'gray',
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: gray500 },
};
......@@ -6,6 +6,4 @@ fragment OnCallParticipant on OncallParticipantType {
username
avatarUrl
}
colorWeight
colorPalette
}
......@@ -3,7 +3,11 @@ import { startCase } from 'lodash';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { newDateAsLocaleTime } from '~/lib/utils/datetime_utility';
import { sprintf, __ } from '~/locale';
import { ASSIGNEE_COLORS_COMBO, LIGHT_TO_DARK_MODE_SHADE_MAPPING } from '../constants';
import {
ASSIGNEE_COLORS_COMBO,
LIGHT_TO_DARK_MODE_SHADE_MAPPING,
NON_ACTIVE_PARTICIPANT_STYLE,
} from '../constants';
/**
* Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska
......@@ -33,39 +37,21 @@ export const isNameFieldValid = (nameField) => {
return Boolean(nameField?.length);
};
/**
* Returns a Array of Objects that represent the shift participant
* with his/her username and unique shift color values
*
* @param {Object[]} participants
*
* @returns {Object[]} A list of values to save each participant
* @property {string} username
* @property {string} colorWeight
* @property {string} colorPalette
*/
export const getParticipantsForSave = (participants) =>
participants.map(({ username, colorWeight, colorPalette }) => ({
username,
// eslint-disable-next-line @gitlab/require-i18n-strings
colorWeight: `WEIGHT_${colorWeight}`,
colorPalette: colorPalette.toUpperCase(),
}));
/**
* Returns user data along with user token styles - color of the text
* as well as the token background color depending on light or dark mode
*
* @template User
* @param {User} user
* @param {Object}
* @property {string} colorWeight
* @property {string} colorPalette
*
* @returns {Object}
* @property {User}
* @property {string} class (CSS) for text color
* @property {string} styles for token background color
* @property {string} colorWeight
* @property {string} colorPalette
* @property {string} textClass (CSS) for text color
* @property {string} backgroundStyle for background color
*/
export const getUserTokenStyles = (user) => {
const { colorWeight, colorPalette } = user;
export const getShiftStyles = ({ colorWeight, colorPalette }) => {
const isDarkMode = darkModeEnabled();
const modeColorWeight = isDarkMode ? LIGHT_TO_DARK_MODE_SHADE_MAPPING[colorWeight] : colorWeight;
const bgColor = `dataViz${startCase(colorPalette)}${modeColorWeight}`;
......@@ -78,61 +64,53 @@ export const getUserTokenStyles = (user) => {
}
return {
...user,
class: textClass,
style: { backgroundColor: cssVariables[bgColor] },
textClass,
backgroundStyle: { backgroundColor: cssVariables[bgColor] },
};
};
/**
* Sets colorWeight and colorPalette for all participants options taking into account saved participants colors
* so that there will be no color overlap
*
* @param {Object[]} allParticipants
* @param {Object[]} selectedParticipants
* @param {number} participantIndex
* @returns {Object}
* @property {string} colorWeight
* @property {string} colorPalette
* @property {string} textClass (CSS) for text color
* @property {Object} backgroundStyle for background color
* */
export const getParticipantColor = (participantIndex) => {
if (participantIndex === -1) return NON_ACTIVE_PARTICIPANT_STYLE;
const colorIndexReference = participantIndex % ASSIGNEE_COLORS_COMBO.length;
return getShiftStyles(ASSIGNEE_COLORS_COMBO[colorIndexReference]);
};
/**
* Returns a Array of Objects that represent the shift participant
* with his/her username and unique shift color values
*
* @param {Object[]} participants
*
* @returns {Object[]} A list of all participants with colorWeight and colorPalette properties set
* @returns {Object[]} A list of values to save each participant
* @property {string} username
* @property {string} colorWeight
* @property {string} colorPalette
*/
export const setParticipantsColors = (allParticipants, selectedParticipants = []) => {
// filter out the colors that saved participants have assigned
// so there are no duplicate colors
let availableColors = ASSIGNEE_COLORS_COMBO.filter(({ colorWeight, colorPalette }) =>
selectedParticipants.every(
({ colorWeight: weight, colorPalette: palette }) =>
!(colorWeight === weight && colorPalette === palette),
),
);
// if all colors are exhausted, we allow to pick from the whole palette
if (!availableColors.length) {
availableColors = ASSIGNEE_COLORS_COMBO;
}
export const getParticipantsForSave = (participants) => {
/**
* Todo: Remove getParticipantsForSave once styling is no longer
* required in API. See https://gitlab.com/gitlab-org/gitlab/-/issues/344950
*/
const TEMP_DEFAULT_COLOR_WEIGHT = 'WEIGHT_500';
const TEMP_DEFAULT_COLOR_PALLET = 'BLUE';
// filter out participants that were not saved previously and have no color info assigned
// and assign each one an available color set
const participants = allParticipants
.filter((participant) =>
selectedParticipants.every(({ user: { username } }) => username !== participant.username),
)
.map((participant, index) => {
const colorIndex = index % availableColors.length;
const { colorWeight, colorPalette } = availableColors[colorIndex];
return {
...participant,
colorWeight,
colorPalette,
};
});
return [
...participants,
...selectedParticipants.map(({ user, colorWeight, colorPalette }) => ({
...user,
colorWeight,
colorPalette,
})),
].map(getUserTokenStyles);
return participants.map(({ username }) => ({
username,
colorWeight: TEMP_DEFAULT_COLOR_WEIGHT,
colorPalette: TEMP_DEFAULT_COLOR_PALLET,
}));
};
/**
......@@ -167,3 +145,25 @@ export const parseRotationDate = (dateTimeString, scheduleTimezone) => {
return { date, time };
};
/**
* Renames keys to be read by gl-token-selector
* @param {Object[]} participants
*
* @returns {Object}
* @property {Object} user
* @property {string} class (CSS) for text color
* @property {string} styles for token background color
*
*/
export const formatParticipantsForTokenSelector = (participants) => {
return participants.map((item, index) => {
const { textClass, backgroundStyle } = getParticipantColor(index);
return {
...item,
class: textClass,
style: backgroundStyle,
};
});
};
......@@ -4,10 +4,13 @@ import {
getParticipantsForSave,
parseHour,
parseRotationDate,
getUserTokenStyles,
setParticipantsColors,
getShiftStyles,
getParticipantColor,
formatParticipantsForTokenSelector,
} from 'ee/oncall_schedules/utils/common_utils';
import { ASSIGNEE_COLORS_COMBO } from 'ee/oncall_schedules/constants';
import * as ColorUtils from '~/lib/utils/color_utils';
import { mockParticipants } from './mock_data';
describe('getFormattedTimezone', () => {
it('formats the timezone', () => {
......@@ -18,22 +21,24 @@ describe('getFormattedTimezone', () => {
});
describe('getParticipantsForSave', () => {
/**
* Todo: Remove getParticipantsForSave once styling is no longer
* required in API. See https://gitlab.com/gitlab-org/gitlab/-/issues/344950
*/
it('returns participant shift color data along with the username', () => {
const participants = [
{ username: 'user1', colorWeight: 300, colorPalette: 'blue', extraProp: '1' },
{ username: 'user2', colorWeight: 400, colorPalette: 'red', extraProp: '2' },
{ username: 'user3', colorWeight: 500, colorPalette: 'green', extraProp: '4' },
];
const expectedParticipantsForSave = [
{ username: 'user1', colorWeight: 'WEIGHT_300', colorPalette: 'BLUE' },
{ username: 'user2', colorWeight: 'WEIGHT_400', colorPalette: 'RED' },
{ username: 'user3', colorWeight: 'WEIGHT_500', colorPalette: 'GREEN' },
{ username: mockParticipants[0].username, colorWeight: 'WEIGHT_500', colorPalette: 'BLUE' },
{ username: mockParticipants[1].username, colorWeight: 'WEIGHT_500', colorPalette: 'BLUE' },
{ username: mockParticipants[2].username, colorWeight: 'WEIGHT_500', colorPalette: 'BLUE' },
{ username: mockParticipants[3].username, colorWeight: 'WEIGHT_500', colorPalette: 'BLUE' },
{ username: mockParticipants[4].username, colorWeight: 'WEIGHT_500', colorPalette: 'BLUE' },
{ username: mockParticipants[5].username, colorWeight: 'WEIGHT_500', colorPalette: 'BLUE' },
];
expect(getParticipantsForSave(participants)).toEqual(expectedParticipantsForSave);
expect(getParticipantsForSave(mockParticipants)).toEqual(expectedParticipantsForSave);
});
});
describe('getUserTokenStyles', () => {
describe('getShiftStyles', () => {
it.each`
isDarkMode | colorWeight | expectedTextClass | expectedBackgroundColor
${true} | ${900} | ${'gl-text-white'} | ${'#d4dcfa'}
......@@ -45,81 +50,90 @@ describe('getUserTokenStyles', () => {
({ isDarkMode, colorWeight, expectedTextClass, expectedBackgroundColor }) => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => isDarkMode);
const user = { username: 'user1', colorWeight, colorPalette: 'blue' };
const user = { colorWeight, colorPalette: 'blue' };
expect(getUserTokenStyles(user)).toMatchObject({
class: expectedTextClass,
style: { backgroundColor: expectedBackgroundColor },
expect(getShiftStyles(user)).toMatchObject({
textClass: expectedTextClass,
backgroundStyle: { backgroundColor: expectedBackgroundColor },
});
},
);
});
describe('setParticipantsColors', () => {
it('sets token color data to each of the eparticipant', () => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => false);
describe('getParticipantColor', () => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => false);
it.each`
isDarkMode | colorWeight | expectedTextClass | expectedBackgroundColor
${true} | ${900} | ${'gl-text-white'} | ${'#d4dcfa'}
${true} | ${500} | ${'gl-text-gray-900'} | ${'#5772ff'}
${false} | ${400} | ${'gl-text-white'} | ${'#748eff'}
${false} | ${700} | ${'gl-text-white'} | ${'#3547de'}
`(
'sets correct styles and class',
({ isDarkMode, colorWeight, expectedTextClass, expectedBackgroundColor }) => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => isDarkMode);
const userColors = { colorWeight, colorPalette: 'blue' };
expect(getShiftStyles(userColors)).toMatchObject({
textClass: expectedTextClass,
backgroundStyle: { backgroundColor: expectedBackgroundColor },
});
},
);
it('sets token colors for each participant', () => {
const createParticipantsList = (numberOfParticipants) =>
new Array(numberOfParticipants)
.fill(undefined)
.map((item, index) => ({ username: `user${index + 1}`, ...item }));
const participants = createParticipantsList(6);
const allParticpants = [
{ username: 'user1' },
{ username: 'user2' },
{ username: 'user3' },
{ username: 'user4' },
{ username: 'user5' },
{ username: 'user6' },
];
const selectedParticpants = [
{ user: { username: 'user2' }, colorPalette: 'blue', colorWeight: '500' },
{ user: { username: 'user4' }, colorPalette: 'magenta', colorWeight: '500' },
{ user: { username: 'user5' }, colorPalette: 'orange', colorWeight: '500' },
];
const expectedParticipants = [
{
username: 'user1',
colorWeight: '500',
colorPalette: 'aqua',
class: 'gl-text-white',
style: { backgroundColor: '#0094b6' },
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#5772ff' },
},
{
username: 'user3',
colorWeight: '500',
colorPalette: 'green',
class: 'gl-text-white',
style: { backgroundColor: '#608b2f' },
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#d14e00' },
},
{
username: 'user6',
colorWeight: '600',
colorPalette: 'blue',
class: 'gl-text-white',
style: { backgroundColor: '#445cf2' },
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#0094b6' },
},
{
username: 'user2',
colorWeight: '500',
colorPalette: 'blue',
class: 'gl-text-white',
style: { backgroundColor: '#5772ff' },
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#608b2f' },
},
{
username: 'user4',
colorWeight: '500',
colorPalette: 'magenta',
class: 'gl-text-white',
style: { backgroundColor: '#d84280' },
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#d84280' },
},
{
username: 'user5',
colorWeight: '500',
colorPalette: 'orange',
class: 'gl-text-white',
style: { backgroundColor: '#d14e00' },
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#445cf2' },
},
];
expect(setParticipantsColors(allParticpants, selectedParticpants)).toEqual(
expect(participants.map((item, index) => getParticipantColor(index))).toEqual(
expectedParticipants,
);
});
it('when all colors are exhausted it uses the first color in list again', () => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => false);
const numberOfColorCombinations = ASSIGNEE_COLORS_COMBO.length;
const lastParticipantColor = {
textClass: 'gl-text-white',
backgroundStyle: { backgroundColor: '#5772ff' },
};
expect(getParticipantColor(numberOfColorCombinations)).toEqual(lastParticipantColor);
});
});
describe('parseRotationDate', () => {
......@@ -149,3 +163,85 @@ describe('parseHour', () => {
expect(hourInt).toBe(14);
});
});
describe('formatParticipantsForTokenSelector', () => {
it('formats participants in light mode', () => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => false);
const formattedParticipants = formatParticipantsForTokenSelector(mockParticipants);
const expected = [
{
...mockParticipants[0],
class: 'gl-text-white',
style: { backgroundColor: '#5772ff' },
},
{
...mockParticipants[1],
class: 'gl-text-white',
style: { backgroundColor: '#d14e00' },
},
{
...mockParticipants[2],
class: 'gl-text-white',
style: { backgroundColor: '#0094b6' },
},
{
...mockParticipants[3],
class: 'gl-text-white',
style: { backgroundColor: '#608b2f' },
},
{
...mockParticipants[4],
class: 'gl-text-white',
style: { backgroundColor: '#d84280' },
},
{
...mockParticipants[5],
class: 'gl-text-white',
style: { backgroundColor: '#445cf2' },
},
];
expect(formattedParticipants).toStrictEqual(expected);
});
it('formats participants in dark mode', () => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
const formattedParticipants = formatParticipantsForTokenSelector(mockParticipants);
const expected = [
{
...mockParticipants[0],
class: 'gl-text-gray-900',
style: { backgroundColor: '#5772ff' },
},
{
...mockParticipants[1],
class: 'gl-text-gray-900',
style: { backgroundColor: '#d14e00' },
},
{
...mockParticipants[2],
class: 'gl-text-gray-900',
style: { backgroundColor: '#0094b6' },
},
{
...mockParticipants[3],
class: 'gl-text-gray-900',
style: { backgroundColor: '#608b2f' },
},
{
...mockParticipants[4],
class: 'gl-text-gray-900',
style: { backgroundColor: '#d84280' },
},
{
...mockParticipants[5],
class: 'gl-text-white',
style: { backgroundColor: '#748eff' },
},
];
expect(formattedParticipants).toStrictEqual(expected);
});
});
export const mockParticipants = [
{
id: 'gid://gitlab/User/8',
name: 'Carlee Franecki',
username: 'ronnie',
avatarUrl: 'https://www.gravatar.com/avatar/0ae08d010d468d2a962eb74f30da279b?s=80&d=identicon',
},
{
id: 'gid://gitlab/User/20',
name: 'Tori Sporer',
username: 'anjanette',
avatarUrl: 'https://www.gravatar.com/avatar/9a1908ccba696bbfc6c8c629a1cab4df?s=80&d=identicon',
},
{
id: 'gid://gitlab/User/7',
name: 'Aleta Heidenreich',
username: 'naomi',
avatarUrl: 'https://www.gravatar.com/avatar/b6f25e54ae55da7ee9946ca6c75f2ccd?s=80&d=identicon',
},
{
id: 'gid://gitlab/User/15',
name: 'Gerda Jerde',
username: 'daysi',
avatarUrl: 'https://www.gravatar.com/avatar/e4d298bd1599379a59746ef9ac5fcb45?s=80&d=identicon',
},
{
id: 'gid://gitlab/User/10',
name: 'Hermine Beer',
username: 'victor',
avatarUrl: 'https://www.gravatar.com/avatar/6333d7f2ee67e9cdf427494a20d87f78?s=80&d=identicon',
},
{
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
];
......@@ -11,8 +11,6 @@ export const participants = [
avatar: '',
avatarUrl: '',
webUrl: '',
colorWeight: '500',
colorPalette: 'blue',
},
{
id: '2',
......@@ -21,8 +19,6 @@ export const participants = [
avatar: '',
avatarUrl: '',
webUrl: '',
colorWeight: '300',
colorPalette: 'orange',
},
];
......
......@@ -21,9 +21,17 @@
"username": "nora.schaden",
"avatarUrl": "/url",
"name": "nora"
},
"colorWeight": "500",
"colorPalette": "blue"
}
},
{
"__typename": "OncallParticipantType",
"id": "2",
"user": {
"id": "gid://gitlab/User/2",
"username": "racheal.loving",
"avatarUrl": "/url",
"name": "racheal"
}
}
]
},
......@@ -32,11 +40,9 @@
{
"participant": {
"__typename": "OncallParticipantType",
"id": "1",
"colorWeight": "500",
"colorPalette": "blue",
"id": "49",
"user": {
"id": "gid://gitlab/User/1",
"id": "gid://gitlab/IncidentManagement::OncallParticipant/49",
"username": "nora.schaden",
"avatarUrl": "/url",
"name": "nora"
......@@ -49,8 +55,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "2",
"colorWeight": "500",
"colorPalette": "orange",
"user": {
"id": "gid://gitlab/User/2",
"username": "racheal.loving",
......@@ -80,15 +84,23 @@
"nodes": [
{
"__typename": "OncallParticipantType",
"id": "99",
"id": "38",
"user": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/99",
"id": "gid://gitlab/User/38",
"avatarUrl": "url",
"username": "david.oregan",
"avatarUrl": "/url",
"name": "david"
},
"colorWeight": "500",
"colorPalette": "aqua"
}
},
{
"__typename": "OncallParticipantType",
"id": "39",
"user": {
"id": "gid://gitlab/User/39",
"avatarUrl": "url",
"username": "david.keagan",
"name": "david k"
}
}
]
},
......@@ -98,8 +110,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "38",
"colorWeight": "500",
"colorPalette": "aqua",
"user": {
"id": "gid://gitlab/User/38",
"avatarUrl": "url",
......@@ -114,8 +124,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "39",
"colorWeight": "500",
"colorPalette": "green",
"user": {
"id": "gid://gitlab/User/39",
"avatarUrl": "url",
......@@ -151,9 +159,17 @@
"username": "root",
"avatarUrl": "/url",
"name": "Administrator"
},
"colorWeight": "500",
"colorPalette": "magenta"
}
},
{
"__typename": "OncallParticipantType",
"id": "41",
"user": {
"id": "gid://gitlab/User/41",
"avatarUrl": "url",
"username": "root2",
"name": "Administrator 2"
}
}
]
},
......@@ -163,8 +179,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "40",
"colorWeight": "500",
"colorPalette": "magenta",
"user": {
"id": "gid://gitlab/User/40",
"avatarUrl": "url",
......@@ -179,8 +193,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "41",
"colorWeight": "600",
"colorPalette": "blue",
"user": {
"id": "gid://gitlab/User/41",
"avatarUrl": "url",
......@@ -210,15 +222,23 @@
"nodes": [
{
"__typename": "OncallParticipantType",
"id": "51",
"id": "43",
"user": {
"id": "gid://gitlab/IncidentManagement::OncallParticipant/51",
"id": "gid://gitlab/User/43",
"avatarUrl": "url",
"username": "oregand",
"avatarUrl": "/url",
"name": "david"
},
"colorWeight": "600",
"colorPalette": "orange"
}
},
{
"__typename": "OncallParticipantType",
"id": "44",
"user": {
"id": "gid://gitlab/User/44",
"avatarUrl": "url",
"username": "sarah.w",
"name": "sarah"
}
}
]
},
......@@ -228,8 +248,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "43",
"colorWeight": "600",
"colorPalette": "orange",
"user": {
"id": "gid://gitlab/User/43",
"avatarUrl": "url",
......@@ -244,8 +262,6 @@
"participant": {
"__typename": "OncallParticipantType",
"id": "44",
"colorWeight": "600",
"colorPalette": "aqua",
"user": {
"id": "gid://gitlab/User/44",
"avatarUrl": "url",
......
......@@ -2,6 +2,7 @@ import { GlAlert, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import { cloneDeep } from 'lodash';
import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue';
import AddEditRotationModal, {
i18n,
......@@ -377,8 +378,21 @@ describe('AddEditRotationModal', () => {
});
it('should load participants correctly', () => {
expect(findForm().props('form')).toMatchObject({
participants: [{ name: 'nora' }],
expect(cloneDeep(findForm().props('form'))).toMatchObject({
participants: [
{
id: 'gid://gitlab/IncidentManagement::OncallParticipant/49',
username: 'nora.schaden',
avatarUrl: '/url',
name: 'nora',
},
{
id: 'gid://gitlab/User/2',
username: 'racheal.loving',
avatarUrl: '/url',
name: 'racheal',
},
],
});
});
......
......@@ -4,6 +4,7 @@ import RotationAssignee, {
SHIFT_WIDTHS,
TIME_DATE_FORMAT,
} from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import { getParticipantColor } from 'ee/oncall_schedules/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { formatDate } from '~/lib/utils/datetime_utility';
import { truncate } from '~/lib/utils/text_utility';
......@@ -15,7 +16,7 @@ jest.mock('~/lib/utils/color_utils');
describe('RotationAssignee', () => {
let wrapper;
const shiftWidth = SHIFT_WIDTHS.md;
const containerStyle = { width: SHIFT_WIDTHS.md, left: 0 };
const assignee = mockRotations[0].shifts.nodes[0];
const findToken = () => wrapper.findByTestId('rotation-assignee');
const findAvatar = () => wrapper.findComponent(GlAvatar);
......@@ -27,16 +28,17 @@ describe('RotationAssignee', () => {
const formattedDate = (date) => {
return formatDate(date, TIME_DATE_FORMAT);
};
const color = getParticipantColor(0);
function createComponent({ props = {} } = {}) {
wrapper = extendedWrapper(
shallowMount(RotationAssignee, {
propsData: {
assignee: { ...assignee.participant },
rotationAssigneeStartsAt: assignee.startsAt,
rotationAssigneeEndsAt: assignee.endsAt,
rotationAssigneeStyle: { left: '0px', width: `${shiftWidth}px` },
shiftWidth,
startsAt: assignee.startsAt,
endsAt: assignee.endsAt,
containerStyle,
color,
...props,
},
}),
......@@ -54,22 +56,24 @@ describe('RotationAssignee', () => {
describe('rotation assignee token', () => {
it('should render an assignee name and avatar', () => {
const LARGE_SHIFT_WIDTH = 150;
createComponent({ props: { shiftWidth: LARGE_SHIFT_WIDTH } });
createComponent({ props: { containerStyle: { width: `${LARGE_SHIFT_WIDTH}px` } } });
expect(findAvatar().props('src')).toBe(assignee.participant.user.avatarUrl);
expect(findName().text()).toBe(assignee.participant.user.username);
});
it('truncate the rotation name on small screens', () => {
createComponent({ props: { containerStyle: { width: `${SHIFT_WIDTHS.md}px` } } });
expect(findName().text()).toBe(truncate(assignee.participant.user.username, 3));
});
it('hides the rotation name on mobile screens', () => {
createComponent({ props: { shiftWidth: SHIFT_WIDTHS.sm } });
createComponent({ props: { containerStyle: { width: `${SHIFT_WIDTHS.sm}px` } } });
expect(findName().exists()).toBe(false);
});
it('hides the avatar on the smallest screens', () => {
createComponent({ props: { shiftWidth: SHIFT_WIDTHS.xs } });
createComponent({ props: { containerStyle: { width: `${SHIFT_WIDTHS.xs}px` } } });
expect(findAvatar().exists()).toBe(false);
});
......@@ -80,8 +84,10 @@ describe('RotationAssignee', () => {
it('should render an assignee schedule and rotation information in a popover', () => {
expect(findPopOver().attributes('target')).toBe('rotation-assignee-fakeUniqueId');
expect(findStartsAt().text()).toContain(formattedDate(assignee.startsAt));
expect(findEndsAt().text()).toContain(formattedDate(assignee.endsAt));
expect(findStartsAt().text()).toContain(`Starts: ${formattedDate(assignee.startsAt)}`);
expect(findEndsAt().text()).toContain(
formattedDate(`Ends: ${formattedDate(assignee.endsAt)}`),
);
});
});
});
import { GlCard } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import RotationsAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import RotationAssignee from 'ee/oncall_schedules/components/rotations/components/rotation_assignee.vue';
import CurrentDayIndicator from 'ee/oncall_schedules/components/schedule/components/current_day_indicator.vue';
import RotationsListSection from 'ee/oncall_schedules/components/schedule/components/rotations_list_section.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
......@@ -45,7 +45,7 @@ describe('RotationsListSectionComponent', () => {
}
const findTimelineCells = () => wrapper.findAllByTestId('timeline-cell');
const findRotationAssignees = () => wrapper.findAllComponents(RotationsAssignee);
const findRotationAssignees = () => wrapper.findAllComponents(RotationAssignee);
const findCurrentDayIndicatorContent = () => wrapper.findByTestId('current-day-indicator');
const findRotationName = (id) => wrapper.findByTestId(`rotation-name-${id}`);
const findRotationNameTooltip = (id) => getBinding(findRotationName(id).element, 'gl-tooltip');
......
......@@ -3,14 +3,11 @@ import RotationsAssignee from 'ee/oncall_schedules/components/rotations/componen
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 { nDaysAfter } from '~/lib/utils/datetime_utility';
import { getParticipantColor } from 'ee/oncall_schedules/utils/common_utils';
import mockRotations from '../../../../mocks/mock_rotation.json';
const shift = {
participant: {
id: '1',
user: {
username: 'nora.schaden',
},
},
participant: mockRotations[0].shifts.nodes[0].participant,
// 3.5 days
startsAt: '2021-01-15T04:00:00.000Z',
endsAt: '2021-01-15T06:00:00.000Z', // absolute shift length is 2 hours(7200000 milliseconds)
......@@ -19,10 +16,10 @@ const shift = {
const CELL_WIDTH = 50;
const timeframeItem = new Date(2021, 0, 13); // Timeframe starts on the 13th
const timeframe = [timeframeItem, new Date(nDaysAfter(timeframeItem, DAYS_IN_WEEK))];
const shiftColor = getParticipantColor(0);
describe('ee/oncall_schedules/components/schedule/components/shifts/components/shift_item.vue', () => {
let wrapper;
function createComponent({ props = {} } = {}) {
wrapper = shallowMount(ShiftItem, {
propsData: {
......@@ -30,6 +27,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
timeframe,
presetType: PRESET_TYPES.WEEKS, // Total grid time in MS: 1209600000
timelineWidth: CELL_WIDTH * 14, // Total grid width in px: 700
shiftColor,
...props,
},
});
......@@ -53,20 +51,14 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
// See `getPixelWidth`
// const width = ((durationMillis + DLSOffset) * timelineWidth) / totalTime;
// ((7200000 + 0) * 700) / 1209600000
expect(findRotationAssignee().props('rotationAssigneeStyle').width).toBe('4px');
});
it('should calculate a shift width the same as rotation assignee child components width', () => {
expect(findRotationAssignee().props('shiftWidth')).toBe(4);
expect(findRotationAssignee().props('containerStyle').width).toBe('4px');
});
it('should a rotation assignee child components offset based on its absolute time', () => {
// See `getPixelOffset`
// const left = (timelineWidth * timeOffset) / totalTime;
// (700 * 187200000) / 1209600000
const rotationAssigneeOffset = parseFloat(
findRotationAssignee().props('rotationAssigneeStyle').left,
);
const rotationAssigneeOffset = parseFloat(findRotationAssignee().props('containerStyle').left);
expect(rotationAssigneeOffset).toBeCloseTo(108.33);
});
});
......@@ -3,7 +3,7 @@ import {
getTimeOffset,
getDuration,
getPixelOffset,
getPixelWidth,
getShiftContainerStyles,
milliseconds,
} from 'ee/oncall_schedules/components/schedule/components/shifts/components/shift_utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
......@@ -21,6 +21,8 @@ const TWELVE_HOURS = 12 * ONE_HOUR;
const ONE_DAY = 2 * TWELVE_HOURS;
const TWO_WEEKS = 14 * ONE_DAY;
const timeframe = [new Date('2021-01-13T00:00:00.000Z'), new Date('2021-01-14T00:00:00.000Z')];
describe('~ee/oncall_schedules/components/schedule/components/shifts/components/shift_utils.js', () => {
describe('milliseconds', () => {
const mockDSLOffset = { m: 300 };
......@@ -57,10 +59,6 @@ describe('~ee/oncall_schedules/components/schedule/components/shifts/components/
describe('getPixelOffset', () => {
it('calculates the correct pixel offest', () => {
const timeframe = [
new Date('2021-01-13T00:00:00.000Z'),
new Date('2021-01-14T00:00:00.000Z'),
];
const timelineWidth = 1000;
const presetType = PRESET_TYPES.DAYS;
const pixelOffset = getPixelOffset({
......@@ -73,18 +71,20 @@ describe('~ee/oncall_schedules/components/schedule/components/shifts/components/
});
});
describe('getPixelWidth', () => {
describe('getShiftContainerStyles', () => {
it('calculates the correct pixel width', () => {
const timelineWidth = 1200; // 50 pixels per hour
const presetType = PRESET_TYPES.DAYS;
const shiftDLSOffset = 60; // one hour
const pixelWidth = getPixelWidth({
shift: mockShift,
timelineWidth,
const containerStyles = getShiftContainerStyles({
timeframe,
presetType,
shiftDLSOffset,
timelineWidth,
shift: mockShift,
});
expect(containerStyles).toStrictEqual({
left: `600px`,
width: `400px`,
});
expect(pixelWidth).toBe(450); // 7 hrs
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment