Commit 276e64be authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Jose Ivan Vargas

Add colors for rotation participants

parent 523cb9df
...@@ -8,9 +8,11 @@ import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/qu ...@@ -8,9 +8,11 @@ import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/qu
import { updateStoreAfterRotationEdit } from 'ee/oncall_schedules/utils/cache_updates'; import { updateStoreAfterRotationEdit } from 'ee/oncall_schedules/utils/cache_updates';
import { import {
isNameFieldValid, isNameFieldValid,
getParticipantsForSave,
parseHour, parseHour,
parseRotationDate, parseRotationDate,
setParticipantsColors,
getUserTokenStyles,
getParticipantsForSave,
} from 'ee/oncall_schedules/utils/common_utils'; } from 'ee/oncall_schedules/utils/common_utils';
import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql'; import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql';
import { format24HourTimeStringFromInt, formatDate } from '~/lib/utils/datetime_utility'; import { format24HourTimeStringFromInt, formatDate } from '~/lib/utils/datetime_utility';
...@@ -151,7 +153,6 @@ export default { ...@@ -151,7 +153,6 @@ export default {
isRestrictedToTime, isRestrictedToTime,
restrictedTo: { startTime: activeStartTime, endTime: activeEndTime }, restrictedTo: { startTime: activeStartTime, endTime: activeEndTime },
} = this.form; } = this.form;
const variables = { const variables = {
name, name,
participants: getParticipantsForSave(participants), participants: getParticipantsForSave(participants),
...@@ -198,6 +199,9 @@ export default { ...@@ -198,6 +199,9 @@ export default {
} }
return false; return false;
}, },
participantsWithTokenStylesData() {
return setParticipantsColors(this.participants, this.rotation?.participants?.nodes);
},
}, },
methods: { methods: {
createRotation() { createRotation() {
...@@ -319,7 +323,9 @@ export default { ...@@ -319,7 +323,9 @@ export default {
this.form.name = this.rotation.name; this.form.name = this.rotation.name;
const participants = const participants =
this.rotation?.participants?.nodes?.map(({ user }) => ({ ...user })) ?? []; this.rotation?.participants?.nodes?.map(({ user, colorWeight, colorPalette }) =>
getUserTokenStyles({ ...user, colorWeight, colorPalette }),
) ?? [];
this.form.participants = participants; this.form.participants = participants;
this.form.rotationLength = { this.form.rotationLength = {
...@@ -366,7 +372,7 @@ export default { ...@@ -366,7 +372,7 @@ export default {
:validation-state="validationState" :validation-state="validationState"
:form="form" :form="form"
:schedule="schedule" :schedule="schedule"
:participants="participants" :participants="participantsWithTokenStylesData"
:is-loading="isLoading" :is-loading="isLoading"
@update-rotation-form="updateRotationForm" @update-rotation-form="updateRotationForm"
@filter-participants="filterParticipants" @filter-participants="filterParticipants"
......
...@@ -17,16 +17,10 @@ export const LIGHT_TO_DARK_MODE_SHADE_MAPPING = { ...@@ -17,16 +17,10 @@ export const LIGHT_TO_DARK_MODE_SHADE_MAPPING = {
950: 50, 950: 50,
}; };
/** export const ASSIGNEE_COLORS_COMBO = CHEVRON_SKIPPING_SHADE_ENUM.map((weight) =>
* an Array of Objects that represent the 30 possible
* color combinations for assignees
* @type {{colorWeight: string, colorPalette: string}[]}
*/
export const ASSIGNEE_COLORS_COMBO = CHEVRON_SKIPPING_SHADE_ENUM.map((shade) =>
CHEVRON_SKIPPING_PALETTE_ENUM.map((color) => ({ CHEVRON_SKIPPING_PALETTE_ENUM.map((color) => ({
// eslint-disable-next-line @gitlab/require-i18n-strings colorWeight: weight,
colorWeight: `WEIGHT_${shade.toUpperCase()}`, colorPalette: color,
colorPalette: color.toUpperCase(),
})), })),
).flat(); ).flat();
......
import * as cssVariables from '@gitlab/ui/scss_to_js/scss_variables';
import { startCase } from 'lodash';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { newDateAsLocaleTime } from '~/lib/utils/datetime_utility'; import { newDateAsLocaleTime } from '~/lib/utils/datetime_utility';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import { ASSIGNEE_COLORS_COMBO } from '../constants'; import { ASSIGNEE_COLORS_COMBO, LIGHT_TO_DARK_MODE_SHADE_MAPPING } from '../constants';
/** /**
* Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska * Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska
...@@ -35,7 +38,6 @@ export const isNameFieldValid = (nameField) => { ...@@ -35,7 +38,6 @@ export const isNameFieldValid = (nameField) => {
* with his/her username and unique shift color values * with his/her username and unique shift color values
* *
* @param {Object[]} participants * @param {Object[]} participants
* @param {string} participants[].username - The username of the participant.
* *
* @returns {Object[]} A list of values to save each participant * @returns {Object[]} A list of values to save each participant
* @property {string} username * @property {string} username
...@@ -43,16 +45,95 @@ export const isNameFieldValid = (nameField) => { ...@@ -43,16 +45,95 @@ export const isNameFieldValid = (nameField) => {
* @property {string} colorPalette * @property {string} colorPalette
*/ */
export const getParticipantsForSave = (participants) => export const getParticipantsForSave = (participants) =>
participants.map(({ username }, index) => { participants.map(({ username, colorWeight, colorPalette }) => ({
const colorIndex = index % ASSIGNEE_COLORS_COMBO.length; username,
const { colorWeight, colorPalette } = ASSIGNEE_COLORS_COMBO[colorIndex]; // eslint-disable-next-line @gitlab/require-i18n-strings
colorWeight: `WEIGHT_${colorWeight}`,
colorPalette: colorPalette.toUpperCase(),
}));
return { /**
username, * 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
*
* @returns {Object}
* @property {User}
* @property {string} class (CSS) for text color
* @property {string} styles for token background color
*/
export const getUserTokenStyles = (user) => {
const { colorWeight, colorPalette } = user;
const isDarkMode = darkModeEnabled();
const modeColorWeight = isDarkMode ? LIGHT_TO_DARK_MODE_SHADE_MAPPING[colorWeight] : colorWeight;
const bgColor = `dataViz${startCase(colorPalette)}${modeColorWeight}`;
let textClass = 'gl-text-white';
if (isDarkMode) {
const medianColorPaletteWeight = 500;
textClass = modeColorWeight < medianColorPaletteWeight ? 'gl-text-white' : 'gl-text-gray-900';
}
return {
...user,
class: textClass,
style: { 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
*
* @returns {Object[]} A list of all participants with colorWeight and colorPalette properties set
*/
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;
}
// 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, colorWeight,
colorPalette, colorPalette,
}; })),
}); ].map(getUserTokenStyles);
};
/** /**
* Parses a activePeriod string into an integer value * Parses a activePeriod string into an integer value
......
import { ASSIGNEE_COLORS_COMBO } from 'ee/oncall_schedules/constants';
import { import {
getFormattedTimezone, getFormattedTimezone,
getParticipantsForSave, getParticipantsForSave,
parseHour, parseHour,
parseRotationDate, parseRotationDate,
getUserTokenStyles,
setParticipantsColors,
} from 'ee/oncall_schedules/utils/common_utils'; } from 'ee/oncall_schedules/utils/common_utils';
import * as ColorUtils from '~/lib/utils/color_utils';
import mockTimezones from './mocks/mock_timezones.json'; import mockTimezones from './mocks/mock_timezones.json';
describe('getFormattedTimezone', () => { describe('getFormattedTimezone', () => {
...@@ -17,16 +19,106 @@ describe('getFormattedTimezone', () => { ...@@ -17,16 +19,106 @@ describe('getFormattedTimezone', () => {
describe('getParticipantsForSave', () => { describe('getParticipantsForSave', () => {
it('returns participant shift color data along with the username', () => { it('returns participant shift color data along with the username', () => {
const participants = [{ username: 'user1' }, { username: 'user2' }, { username: 'user3' }]; const participants = [
const result = getParticipantsForSave(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' },
];
expect(getParticipantsForSave(participants)).toEqual(expectedParticipantsForSave);
});
});
describe('getUserTokenStyles', () => {
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 user = { username: 'user1', colorWeight, colorPalette: 'blue' };
expect(getUserTokenStyles(user)).toMatchObject({
class: expectedTextClass,
style: { backgroundColor: expectedBackgroundColor },
});
},
);
});
expect(result.length).toBe(participants.length); describe('setParticipantsColors', () => {
it('sets token color data to each of the eparticipant', () => {
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => false);
result.forEach((participant, index) => { const allParticpants = [
const { colorWeight, colorPalette } = ASSIGNEE_COLORS_COMBO[index]; { username: 'user1' },
const { username } = participants[index]; { username: 'user2' },
expect(participant).toEqual({ username, colorWeight, colorPalette }); { 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' },
},
{
username: 'user3',
colorWeight: '500',
colorPalette: 'green',
class: 'gl-text-white',
style: { backgroundColor: '#608b2f' },
},
{
username: 'user6',
colorWeight: '600',
colorPalette: 'blue',
class: 'gl-text-white',
style: { backgroundColor: '#445cf2' },
},
{
username: 'user2',
colorWeight: '500',
colorPalette: 'blue',
class: 'gl-text-white',
style: { backgroundColor: '#5772ff' },
},
{
username: 'user4',
colorWeight: '500',
colorPalette: 'magenta',
class: 'gl-text-white',
style: { backgroundColor: '#d84280' },
},
{
username: 'user5',
colorWeight: '500',
colorPalette: 'orange',
class: 'gl-text-white',
style: { backgroundColor: '#d14e00' },
},
];
expect(setParticipantsColors(allParticpants, selectedParticpants)).toEqual(
expectedParticipants,
);
}); });
}); });
......
...@@ -11,6 +11,8 @@ export const participants = [ ...@@ -11,6 +11,8 @@ export const participants = [
avatar: '', avatar: '',
avatarUrl: '', avatarUrl: '',
webUrl: '', webUrl: '',
colorWeight: '500',
colorPalette: 'blue',
}, },
{ {
id: '2', id: '2',
...@@ -19,6 +21,8 @@ export const participants = [ ...@@ -19,6 +21,8 @@ export const participants = [
avatar: '', avatar: '',
avatarUrl: '', avatarUrl: '',
webUrl: '', webUrl: '',
colorWeight: '300',
colorPalette: 'orange',
}, },
]; ];
......
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
import mockRotation from '../../mocks/mock_rotation.json'; import mockRotation from '../../mocks/mock_rotation.json';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/color_utils');
const schedule = const schedule =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0]; getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
......
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