Commit 197b1de0 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '324719-add-colors-for-rotation-users' into 'master'

Add colors for rotation participants

See merge request gitlab-org/gitlab!67212
parents eefc4edf 276e64be
......@@ -8,9 +8,11 @@ import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/qu
import { updateStoreAfterRotationEdit } from 'ee/oncall_schedules/utils/cache_updates';
import {
isNameFieldValid,
getParticipantsForSave,
parseHour,
parseRotationDate,
setParticipantsColors,
getUserTokenStyles,
getParticipantsForSave,
} from 'ee/oncall_schedules/utils/common_utils';
import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql';
import { format24HourTimeStringFromInt, formatDate } from '~/lib/utils/datetime_utility';
......@@ -151,7 +153,6 @@ export default {
isRestrictedToTime,
restrictedTo: { startTime: activeStartTime, endTime: activeEndTime },
} = this.form;
const variables = {
name,
participants: getParticipantsForSave(participants),
......@@ -198,6 +199,9 @@ export default {
}
return false;
},
participantsWithTokenStylesData() {
return setParticipantsColors(this.participants, this.rotation?.participants?.nodes);
},
},
methods: {
createRotation() {
......@@ -319,7 +323,9 @@ export default {
this.form.name = this.rotation.name;
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.rotationLength = {
......@@ -366,7 +372,7 @@ export default {
:validation-state="validationState"
:form="form"
:schedule="schedule"
:participants="participants"
:participants="participantsWithTokenStylesData"
:is-loading="isLoading"
@update-rotation-form="updateRotationForm"
@filter-participants="filterParticipants"
......
......@@ -17,16 +17,10 @@ export const LIGHT_TO_DARK_MODE_SHADE_MAPPING = {
950: 50,
};
/**
* 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) =>
export const ASSIGNEE_COLORS_COMBO = CHEVRON_SKIPPING_SHADE_ENUM.map((weight) =>
CHEVRON_SKIPPING_PALETTE_ENUM.map((color) => ({
// eslint-disable-next-line @gitlab/require-i18n-strings
colorWeight: `WEIGHT_${shade.toUpperCase()}`,
colorPalette: color.toUpperCase(),
colorWeight: weight,
colorPalette: color,
})),
).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 { 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
......@@ -35,7 +38,6 @@ export const isNameFieldValid = (nameField) => {
* with his/her username and unique shift color values
*
* @param {Object[]} participants
* @param {string} participants[].username - The username of the participant.
*
* @returns {Object[]} A list of values to save each participant
* @property {string} username
......@@ -43,16 +45,95 @@ export const isNameFieldValid = (nameField) => {
* @property {string} colorPalette
*/
export const getParticipantsForSave = (participants) =>
participants.map(({ username }, index) => {
const colorIndex = index % ASSIGNEE_COLORS_COMBO.length;
const { colorWeight, colorPalette } = ASSIGNEE_COLORS_COMBO[colorIndex];
participants.map(({ username, colorWeight, colorPalette }) => ({
username,
// 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,
colorPalette,
};
});
})),
].map(getUserTokenStyles);
};
/**
* Parses a activePeriod string into an integer value
......
import { ASSIGNEE_COLORS_COMBO } from 'ee/oncall_schedules/constants';
import {
getFormattedTimezone,
getParticipantsForSave,
parseHour,
parseRotationDate,
getUserTokenStyles,
setParticipantsColors,
} from 'ee/oncall_schedules/utils/common_utils';
import * as ColorUtils from '~/lib/utils/color_utils';
import mockTimezones from './mocks/mock_timezones.json';
describe('getFormattedTimezone', () => {
......@@ -17,16 +19,106 @@ describe('getFormattedTimezone', () => {
describe('getParticipantsForSave', () => {
it('returns participant shift color data along with the username', () => {
const participants = [{ username: 'user1' }, { username: 'user2' }, { username: 'user3' }];
const result = getParticipantsForSave(participants);
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' },
];
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 { colorWeight, colorPalette } = ASSIGNEE_COLORS_COMBO[index];
const { username } = participants[index];
expect(participant).toEqual({ username, colorWeight, colorPalette });
});
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' },
},
{
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 = [
avatar: '',
avatarUrl: '',
webUrl: '',
colorWeight: '500',
colorPalette: 'blue',
},
{
id: '2',
......@@ -19,6 +21,8 @@ export const participants = [
avatar: '',
avatarUrl: '',
webUrl: '',
colorWeight: '300',
colorPalette: 'orange',
},
];
......
......@@ -20,6 +20,7 @@ import {
import mockRotation from '../../mocks/mock_rotation.json';
jest.mock('~/flash');
jest.mock('~/lib/utils/color_utils');
const schedule =
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