Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -10,6 +10,13 @@ jest.mock("@/features/Calendars/services/helpers");
|
|||||||
jest.mock("@/features/Calendars/CalendarApi");
|
jest.mock("@/features/Calendars/CalendarApi");
|
||||||
jest.mock("@/utils/errorUtils");
|
jest.mock("@/utils/errorUtils");
|
||||||
jest.mock("@/features/Calendars/utils/normalizeCalendar");
|
jest.mock("@/features/Calendars/utils/normalizeCalendar");
|
||||||
|
jest.mock("@/utils/getAccessiblePair", () => ({
|
||||||
|
getAccessiblePair: jest.fn().mockReturnValue("#FFF"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@mui/material/styles", () => ({
|
||||||
|
createTheme: jest.fn().mockReturnValue({}),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock;
|
const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock;
|
||||||
const mockedFetchOwnerData = fetchOwnerData as jest.Mock;
|
const mockedFetchOwnerData = fetchOwnerData as jest.Mock;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAccessiblePair } from "@/components/Calendar/utils/calendarColorsUtils";
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
import { stringAvatar } from "@/components/Event/utils/eventUtils";
|
import { stringAvatar } from "@/components/Event/utils/eventUtils";
|
||||||
import { useUserSearch } from "./useUserSearch";
|
import { useUserSearch } from "./useUserSearch";
|
||||||
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
|
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAccessiblePair } from "@/components/Calendar/utils/calendarColorsUtils";
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
import { useUserSearch } from "./useUserSearch";
|
import { useUserSearch } from "./useUserSearch";
|
||||||
import { ResourceIcon } from "./ResourceIcon";
|
import { ResourceIcon } from "./ResourceIcon";
|
||||||
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
|
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
|||||||
import momentTimezonePlugin from "@fullcalendar/moment-timezone";
|
import momentTimezonePlugin from "@fullcalendar/moment-timezone";
|
||||||
import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import { Box, Button, radius, useTheme } from "@linagora/twake-mui";
|
import { Box, Button, radius } from "@linagora/twake-mui";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
|
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
@@ -38,7 +38,6 @@ import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
|||||||
import { MiniCalendar } from "./MiniCalendar";
|
import { MiniCalendar } from "./MiniCalendar";
|
||||||
import { TempCalendarsInput } from "./TempCalendarsInput";
|
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||||
import { TimezoneSelector } from "./TimezoneSelector";
|
import { TimezoneSelector } from "./TimezoneSelector";
|
||||||
import { updateDarkColor } from "./utils/calendarColorsUtils";
|
|
||||||
import {
|
import {
|
||||||
eventToFullCalendarFormat,
|
eventToFullCalendarFormat,
|
||||||
extractEvents,
|
extractEvents,
|
||||||
@@ -75,7 +74,6 @@ export default function CalendarApp({
|
|||||||
const userId =
|
const userId =
|
||||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const theme = useTheme();
|
|
||||||
const view = useAppSelector((state) => state.settings.view);
|
const view = useAppSelector((state) => state.settings.view);
|
||||||
const userData = useAppSelector((state) => state.user.userData);
|
const userData = useAppSelector((state) => state.user.userData);
|
||||||
const workingDays = useAppSelector(
|
const workingDays = useAppSelector(
|
||||||
@@ -103,13 +101,6 @@ export default function CalendarApp({
|
|||||||
const [selectedCalendars, setSelectedCalendars] =
|
const [selectedCalendars, setSelectedCalendars] =
|
||||||
useState<string[]>(storedCalendars);
|
useState<string[]>(storedCalendars);
|
||||||
|
|
||||||
const calendarLightSignature = useMemo(() => {
|
|
||||||
return Object.values(calendars || {})
|
|
||||||
.map((cal) => `${cal.id}:${cal.color?.light ?? ""}`)
|
|
||||||
.sort()
|
|
||||||
.join("|");
|
|
||||||
}, [calendars]);
|
|
||||||
|
|
||||||
const calendarIdsString = useMemo(
|
const calendarIdsString = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.keys(calendars || {})
|
Object.keys(calendars || {})
|
||||||
@@ -167,22 +158,6 @@ export default function CalendarApp({
|
|||||||
}
|
}
|
||||||
}, [selectedCalendars, calendarIds.length]);
|
}, [selectedCalendars, calendarIds.length]);
|
||||||
|
|
||||||
const prevCalendarLightSignature = useRef<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!calendarLightSignature) {
|
|
||||||
prevCalendarLightSignature.current = calendarLightSignature;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevCalendarLightSignature.current === calendarLightSignature) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevCalendarLightSignature.current = calendarLightSignature;
|
|
||||||
updateDarkColor(calendars || {}, theme, dispatch);
|
|
||||||
}, [calendarLightSignature, calendars, theme, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calendarIds.length === 0) return;
|
if (calendarIds.length === 0) return;
|
||||||
const validCalendarIds = new Set(calendarIds);
|
const validCalendarIds = new Set(calendarIds);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { HexColorPicker } from "react-colorful";
|
import { HexColorPicker } from "react-colorful";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
import { defaultColors } from "@/utils/defaultColors";
|
import { defaultColors } from "@/utils/defaultColors";
|
||||||
|
|
||||||
export function ColorPicker({
|
export function ColorPicker({
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useRef, useState } from "react";
|
|||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { ResponsiveDialog } from "../Dialog";
|
import { ResponsiveDialog } from "../Dialog";
|
||||||
import { ColorPicker } from "./CalendarColorPicker";
|
import { ColorPicker } from "./CalendarColorPicker";
|
||||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
import { defaultColors } from "@/utils/defaultColors";
|
import { defaultColors } from "@/utils/defaultColors";
|
||||||
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
||||||
import { Resource, ResourceSearch } from "../Attendees/ResourceSearch";
|
import { Resource, ResourceSearch } from "../Attendees/ResourceSearch";
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
|||||||
import { ResponsiveDialog } from "../Dialog";
|
import { ResponsiveDialog } from "../Dialog";
|
||||||
import { stringAvatar } from "../Event/utils/eventUtils";
|
import { stringAvatar } from "../Event/utils/eventUtils";
|
||||||
import { ColorPicker } from "./CalendarColorPicker";
|
import { ColorPicker } from "./CalendarColorPicker";
|
||||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
import { defaultColors } from "@/utils/defaultColors";
|
import { defaultColors } from "@/utils/defaultColors";
|
||||||
|
|
||||||
interface CalendarWithOwner {
|
interface CalendarWithOwner {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TextField, useTheme } from "@linagora/twake-mui";
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
||||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
|
|
||||||
const requestControllers = new Map<string, AbortController>();
|
const requestControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { AppDispatch } from "@/app/store";
|
|
||||||
import { updateCalColor } from "@/features/Calendars/CalendarSlice";
|
|
||||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
|
||||||
import { darken, getContrastRatio, lighten, Theme } from "@linagora/twake-mui";
|
|
||||||
import { defaultColors } from "@/utils/defaultColors";
|
|
||||||
|
|
||||||
export function updateDarkColor(
|
|
||||||
calendars: Record<string, Calendar>,
|
|
||||||
theme: Theme,
|
|
||||||
dispatch: AppDispatch
|
|
||||||
) {
|
|
||||||
Object.values(calendars).forEach((cal) => {
|
|
||||||
if (!cal?.color?.light || typeof cal.color.light !== "string") return;
|
|
||||||
const baseColor = cal.color.light;
|
|
||||||
|
|
||||||
const isDefault = Object.values(defaultColors).find(
|
|
||||||
(c) => c.light === baseColor
|
|
||||||
);
|
|
||||||
const darkColor = isDefault
|
|
||||||
? isDefault.dark
|
|
||||||
: getAccessiblePair(baseColor, theme);
|
|
||||||
|
|
||||||
if (cal.color?.dark === darkColor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
updateCalColor({
|
|
||||||
id: cal.id,
|
|
||||||
color: { light: baseColor, dark: darkColor },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAccessiblePair(baseColor: string, theme: Theme): string {
|
|
||||||
if (typeof baseColor !== "string") {
|
|
||||||
return theme.palette.getContrastText("#000");
|
|
||||||
}
|
|
||||||
|
|
||||||
const contrastToBlack = getContrastRatio(baseColor, "#000");
|
|
||||||
const contrastToWhite = getContrastRatio(baseColor, "#fff");
|
|
||||||
const isLight = contrastToBlack > contrastToWhite;
|
|
||||||
|
|
||||||
const adjusted = isLight ? darken(baseColor, 0.6) : lighten(baseColor, 0.7);
|
|
||||||
|
|
||||||
// Check if contrast meets 4.5
|
|
||||||
const contrast = getContrastRatio(baseColor, adjusted);
|
|
||||||
if (contrast >= 4.5) return adjusted;
|
|
||||||
|
|
||||||
if (isLight) {
|
|
||||||
return "#ffffffff";
|
|
||||||
}
|
|
||||||
|
|
||||||
return theme.palette.getContrastText(baseColor);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
|
|||||||
import { getCalendar } from "../CalendarApi";
|
import { getCalendar } from "../CalendarApi";
|
||||||
import { RejectedError } from "../types/RejectedError";
|
import { RejectedError } from "../types/RejectedError";
|
||||||
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
|
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
|
||||||
import { defaultColors } from "@/utils/defaultColors";
|
|
||||||
import { type RootState } from "@/app/store";
|
import { type RootState } from "@/app/store";
|
||||||
|
|
||||||
export const getCalendarDetailAsync = createAsyncThunk<
|
export const getCalendarDetailAsync = createAsyncThunk<
|
||||||
@@ -42,16 +41,15 @@ export const getCalendarDetailAsync = createAsyncThunk<
|
|||||||
match,
|
match,
|
||||||
signal
|
signal
|
||||||
)) as CalendarData;
|
)) as CalendarData;
|
||||||
|
|
||||||
const color = calendar["apple:color"]
|
|
||||||
? { light: calendar["apple:color"], dark: calendar["apple:color"] }
|
|
||||||
: defaultColors[0];
|
|
||||||
const syncToken = calendar._embedded?.["sync-token"];
|
const syncToken = calendar._embedded?.["sync-token"];
|
||||||
|
|
||||||
const items = calendar._embedded?.["dav:item"];
|
const items = calendar._embedded?.["dav:item"];
|
||||||
const events: CalendarEvent[] = Array.isArray(items)
|
const events: CalendarEvent[] = Array.isArray(items)
|
||||||
? items.flatMap((item: CalendarItem) =>
|
? items.flatMap((item: CalendarItem) =>
|
||||||
extractCalendarEvents(item, { cal: calendarStored, color })
|
extractCalendarEvents(item, {
|
||||||
|
cal: calendarStored,
|
||||||
|
color: calendarStored.color,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { CalendarData } from "../types/CalendarData";
|
|||||||
import { RejectedError } from "../types/RejectedError";
|
import { RejectedError } from "../types/RejectedError";
|
||||||
import { normalizeCalendar } from "../utils/normalizeCalendar";
|
import { normalizeCalendar } from "../utils/normalizeCalendar";
|
||||||
import { fetchOwnerData } from "./helpers";
|
import { fetchOwnerData } from "./helpers";
|
||||||
|
import { createTheme } from "@mui/material/styles";
|
||||||
|
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||||
|
|
||||||
|
const theme = createTheme();
|
||||||
|
|
||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
{ importedCalendars: Record<string, Calendar>; errors: string },
|
{ importedCalendars: Record<string, Calendar>; errors: string },
|
||||||
@@ -77,10 +81,11 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
emails: [],
|
emails: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const color = cal["apple:color"]
|
const rawColor = cal["apple:color"];
|
||||||
|
const color = rawColor
|
||||||
? {
|
? {
|
||||||
light: cal["apple:color"],
|
light: rawColor,
|
||||||
dark: "#fff",
|
dark: getAccessiblePair(rawColor, theme),
|
||||||
}
|
}
|
||||||
: defaultColors[0];
|
: defaultColors[0];
|
||||||
|
|
||||||
@@ -126,10 +131,6 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
if (fetchedCal) {
|
if (fetchedCal) {
|
||||||
importedCalendars[id] = {
|
importedCalendars[id] = {
|
||||||
...fetchedCal,
|
...fetchedCal,
|
||||||
color: {
|
|
||||||
...existingCal.color,
|
|
||||||
light: fetchedCal.color?.light,
|
|
||||||
},
|
|
||||||
events: existingCal.events || {},
|
events: existingCal.events || {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { darken, getContrastRatio, lighten, Theme } from "@linagora/twake-mui";
|
||||||
|
|
||||||
|
export function getAccessiblePair(baseColor: string, theme: Theme): string {
|
||||||
|
if (typeof baseColor !== "string") {
|
||||||
|
return theme.palette.getContrastText("#000");
|
||||||
|
}
|
||||||
|
|
||||||
|
const contrastToBlack = getContrastRatio(baseColor, "#000");
|
||||||
|
const contrastToWhite = getContrastRatio(baseColor, "#fff");
|
||||||
|
const isLight = contrastToBlack > contrastToWhite;
|
||||||
|
|
||||||
|
const adjusted = isLight ? darken(baseColor, 0.6) : lighten(baseColor, 0.7);
|
||||||
|
|
||||||
|
// Check if contrast meets 4.5
|
||||||
|
const contrast = getContrastRatio(baseColor, adjusted);
|
||||||
|
if (contrast >= 4.5) return adjusted;
|
||||||
|
|
||||||
|
if (isLight) {
|
||||||
|
return "#ffffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme.palette.getContrastText(baseColor);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user