Color picker rework (#232)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-10-24 01:20:56 +02:00
committed by GitHub
parent 8681e88390
commit 34363562cc
23 changed files with 504 additions and 183 deletions
@@ -82,7 +82,7 @@ describe("Calendar API", () => {
it("patch Calendar", async () => {
const calId = "calId";
const calLink = "/calendars/calId.json";
const color = "calId";
const color = { light: "calIdLight", dark: "calIdDark" };
const name = "new cal";
const desc = "desc";
@@ -96,7 +96,7 @@ describe("Calendar API", () => {
body: JSON.stringify({
"dav:name": "new cal",
"caldav:description": "desc",
"apple:color": "calId",
"apple:color": "calIdLight",
}),
});
});
+11
View File
@@ -31,6 +31,7 @@
"moment-timezone": "^0.5.48",
"openid-client": "^6.5.3",
"react": "^19.1.0",
"react-colorful": "^5.6.1",
"react-dom": "^19.1.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0",
@@ -11795,6 +11796,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"license": "MIT",
+1
View File
@@ -26,6 +26,7 @@
"moment-timezone": "^0.5.48",
"openid-client": "^6.5.3",
"react": "^19.1.0",
"react-colorful": "^5.6.1",
"react-dom": "^19.1.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0",
+4 -3
View File
@@ -10,13 +10,14 @@ import { searchUsers } from "../../features/User/userAPI";
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
import Chip from "@mui/material/Chip";
import { useTheme } from "@mui/material/styles";
import { getAccessiblePair } from "../Calendar/utils/calendarColorsUtils";
export interface User {
email: string;
displayName: string;
avatarUrl: string;
openpaasId: string;
color?: string;
color?: Record<string, string>;
}
export function PeopleSearch({
@@ -150,8 +151,8 @@ export function PeopleSearch({
const label = isString ? option : option.displayName || option.email;
const chipColor = isString
? theme.palette.grey[300]
: (option.color ?? theme.palette.grey[300]);
const textColor = theme.palette.getContrastText(chipColor);
: (option.color?.light ?? theme.palette.grey[300]);
const textColor = getAccessiblePair(chipColor, theme);
return (
<Chip
+12 -1
View File
@@ -40,6 +40,8 @@ import momentTimezonePlugin from "@fullcalendar/moment-timezone";
import { TimezoneSelector } from "./TimezoneSelector";
import { MiniCalendar } from "./MiniCalendar";
import { User } from "../Attendees/PeopleSearch";
import { useTheme } from "@mui/material/styles";
import { updateDarkColor } from "./utils/calendarColorsUtils";
interface CalendarAppProps {
calendarRef: React.RefObject<CalendarApi | null>;
@@ -58,7 +60,7 @@ export default function CalendarApp({
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const dispatch = useAppDispatch();
const theme = useTheme();
useEffect(() => {
if (!tokens || !userId) {
dispatch(push("/"));
@@ -108,6 +110,15 @@ export default function CalendarApp({
}
}, [calendars, userId]);
useEffect(() => {
updateDarkColor(calendars, theme, dispatch);
}, [
theme,
Object.values(calendars)
.map((c) => c.color?.dark)
.join(","),
]);
useEffect(() => {
const validCalendarIds = new Set(Object.keys(calendars));
setSelectedCalendars((prev) =>
+226 -21
View File
@@ -1,36 +1,241 @@
import { Box } from "@mui/material";
const defaultColors = ["#34d399", "#fbbf24", "#f87171", "#60a5fa", "#f472b6"];
import AddIcon from "@mui/icons-material/Add";
import CheckIcon from "@mui/icons-material/Check";
import {
Box,
Button,
Popover,
TextField,
Typography,
useTheme,
} from "@mui/material";
import { useState } from "react";
import { HexColorPicker } from "react-colorful";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
export function ColorPicker({
selectedColor,
colors = defaultColors,
onChange,
}: {
selectedColor?: string;
colors?: string[];
onChange: (color: string) => void;
selectedColor: Record<string, string>;
colors?: Record<string, string>[];
onChange: (color: Record<string, string>) => void;
}) {
const [customColor, setCustomColor] = useState(
!colors.find((c) => c.light === selectedColor?.light)
? selectedColor
: undefined
);
return (
<Box display="flex" alignItems="center" gap={1}>
{colors.map((c) => (
<Box
key={c}
role="button"
aria-label={`select color ${c}`}
onClick={() => onChange(c)}
style={{
width: 20,
height: 20,
borderRadius: "50%",
backgroundColor: c,
cursor: "pointer",
border:
selectedColor === c ? "2px solid black" : "2px solid transparent",
transition: "all 0.2s",
}}
<ColorBox
key={c.light}
color={c}
onChange={onChange}
selectedColor={selectedColor}
/>
))}
{customColor && (
<ColorBox
color={customColor ?? {}}
onChange={onChange}
selectedColor={selectedColor}
/>
)}
<ColorPickerBox
onChange={(c) => {
onChange(c);
setCustomColor(c);
}}
selectedColor={selectedColor}
/>
</Box>
);
}
function ColorBox({
color,
onChange,
selectedColor,
}: {
color: Record<string, string>;
onChange: (color: Record<string, string>) => void;
selectedColor: Record<string, string>;
}) {
return (
<Box
role="button"
aria-label={`select color ${color.light}`}
onClick={() => onChange(color)}
style={{
width: "46px",
height: "32px",
padding: 0,
borderRadius: "4px",
backgroundColor: color.light,
cursor: "pointer",
transition: "all 0.2s",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Box
style={{
height: "7px",
width: "100%",
borderRadius: "4px 4px 0px 0px",
backgroundColor: color.dark,
}}
></Box>
<CheckIcon
style={{
visibility:
selectedColor?.light === color.light ? "visible" : "hidden",
color: color.dark,
}}
/>
</Box>
);
}
function ColorPickerBox({
onChange,
selectedColor,
}: {
onChange: (color: Record<string, string>) => void;
selectedColor: Record<string, string>;
}) {
const [oldColor] = useState(
selectedColor ?? { light: "#ffffff", dark: "#808080" }
);
const [color, setColor] = useState(oldColor);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const theme = useTheme();
const handleClick = (event: any) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
onChange(oldColor);
setAnchorEl(null);
};
const handleSave = () => {
onChange(color);
setAnchorEl(null);
};
const handleColorChange = (c: string) => {
const newLight = c;
const newColor = {
light: newLight,
dark: getAccessiblePair(newLight, theme),
};
setColor(newColor);
onChange(newColor);
};
return (
<>
<Box
key={"colorPicker"}
role="button"
aria-label={`select custom color`}
onClick={handleClick}
style={{
width: "46px",
height: "32px",
padding: 0,
borderRadius: "4px",
border: "1px solid #CBD2E0",
backgroundColor: "#FFF",
cursor: "pointer",
transition: "all 0.2s",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Box
style={{
height: "7px",
width: "100%",
borderRadius: "4px 4px 0px 0px",
backgroundColor: "#CBD2E0",
}}
></Box>
<AddIcon
style={{
color: "#CBD2E0",
}}
/>
</Box>
<Popover
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "center",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
slotProps={{
paper: {
style: {
padding: "24px",
width: "294px",
borderRadius: "8px",
boxShadow: "0px 1px 3px #3C404326",
},
},
}}
>
<Typography variant="subtitle1" fontWeight="600">
Choose custom colour
</Typography>
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
Choose a background colour for this calendar
</Typography>
<Box sx={{ mb: 2 }}>
<HexColorPicker
color={color.light}
onChange={handleColorChange}
style={{ width: "100%" }}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Hex
</Typography>
<TextField
value={color.light?.toUpperCase()}
onChange={(e) => handleColorChange(e.target.value)}
variant="standard"
size="small"
slotProps={{ inputLabel: { shrink: true } }}
/>
</Box>
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
<Button onClick={handleClose}>Cancel</Button>
<Button
variant="contained"
onClick={handleSave}
sx={{ textTransform: "none" }}
>
Save
</Button>
</Box>
</Popover>
</>
);
}
+8 -7
View File
@@ -12,6 +12,7 @@ import { ResponsiveDialog } from "../Dialog";
import { AccessTab } from "./AccessTab";
import { ImportTab } from "./ImportTab";
import { SettingsTab } from "./SettingsTab";
import { defaultColors } from "./utils/calendarColorsUtils";
function CalendarPopover({
open,
@@ -34,7 +35,7 @@ function CalendarPopover({
// existing calendar params
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [color, setColor] = useState("");
const [color, setColor] = useState<Record<string, string>>(defaultColors[0]);
const [visibility, setVisibility] = useState<"private" | "public">("public");
// import tab state
@@ -45,7 +46,7 @@ function CalendarPopover({
// new calendar params (for import new)
const [newCalName, setNewCalName] = useState("");
const [newCalDescription, setNewCalDescription] = useState("");
const [newCalColor, setNewCalColor] = useState("");
const [newCalColor, setNewCalColor] = useState(defaultColors[0]);
const [newCalVisibility, setNewCalVisibility] = useState<
"public" | "private"
>("public");
@@ -55,13 +56,13 @@ function CalendarPopover({
if (calendar) {
setName(calendar.name);
setDescription(calendar.description ?? "");
setColor(calendar.color ?? "");
setColor(calendar.color ?? defaultColors[0]);
setVisibility(calendar.visibility ?? "public");
setImportTarget(calendar.id ?? "new");
} else {
setName("");
setDescription("");
setColor("");
setColor(defaultColors[0]);
setVisibility("public");
setImportTarget("new");
}
@@ -90,7 +91,7 @@ function CalendarPopover({
calId: string,
name: string,
desc: string,
color: string,
color: Record<string, string>,
visibility: string
) => {
await dispatch(
@@ -160,7 +161,7 @@ function CalendarPopover({
onClose(e, reason);
setName("");
setDescription("");
setColor("");
setColor(defaultColors[0]);
setTab("settings");
setVisibility("public");
setImportTarget("new");
@@ -168,7 +169,7 @@ function CalendarPopover({
setNewCalName("");
setNewCalDescription("");
setNewCalColor("");
setNewCalColor(defaultColors[0]);
setNewCalVisibility("public");
};
+99 -112
View File
@@ -16,6 +16,9 @@ import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { ColorPicker } from "./CalendarColorPicker";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
import { useTheme } from "@mui/material/styles";
import { ResponsiveDialog } from "../Dialog";
interface CalendarWithOwner {
cal: Record<string, any>;
@@ -29,8 +32,10 @@ function CalendarItem({
}: {
cal: CalendarWithOwner;
onRemove: () => void;
onColorChange: (color: string) => void;
onColorChange: (color: Record<string, string>) => void;
}) {
const theme = useTheme();
return (
<Box
key={cal.owner.email + cal.cal["dav:name"]}
@@ -65,7 +70,10 @@ function CalendarItem({
<Box display="flex" alignItems="center" gap={1}>
<ColorPicker
selectedColor={cal.cal["apple:color"]}
selectedColor={{
light: cal.cal["apple:color"],
dark: getAccessiblePair(cal.cal["apple:color"], theme),
}}
onChange={onColorChange}
/>
<IconButton size="small" onClick={onRemove}>
@@ -85,7 +93,10 @@ function SelectedCalendarsList({
calendars: Record<string, Calendars>;
selectedCal: CalendarWithOwner[];
onRemove: (cal: CalendarWithOwner) => void;
onColorChange: (cal: CalendarWithOwner, color: string) => void;
onColorChange: (
cal: CalendarWithOwner,
color: Record<string, string>
) => void;
}) {
if (selectedCal.length === 0) return null;
@@ -194,7 +205,13 @@ export default function CalendarSearch({
addSharedCalendarAsync({
userId: openpaasId,
calId,
cal: { ...cal, color: cal.cal["apple:color"] },
cal: {
...cal,
color: {
light: cal.cal["apple:color"],
dark: cal.cal["X-TWAKE-Dark-theme-color"],
},
},
})
);
return cal.cal._links.self.href
@@ -212,122 +229,92 @@ export default function CalendarSearch({
};
return (
<Modal
<ResponsiveDialog
open={open}
contentSx={{ paddingTop: "8px !important" }}
onClose={() => {
onClose({}, "backdropClick");
setSelectedCalendars([]);
setSelectedUsers([]);
}}
title="Browse other calendars"
actions={
<>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button variant="contained" onClick={handleSave}>
Add
</Button>
</>
}
>
<Box
style={{
position: "absolute",
left: "50%",
top: "20%",
transform: "translate( -50%)",
width: "50vw",
maxHeight: "80vh",
<PeopleSearch
objectTypes={["user"]}
selectedUsers={selectedUsers}
onChange={async (event: any, value: User[]) => {
setSelectedUsers(value);
const cals = await Promise.all(
value.map(async (user: User) => {
const cals = (await getCalendars(
user.openpaasId,
"sharedPublic=true&withRights=true"
)) as Record<string, any>;
return cals._embedded?.["dav:calendar"]
? cals._embedded["dav:calendar"].map(
(cal: Record<string, any>) => ({ cal, owner: user })
)
: { cal: undefined, owner: user };
})
);
setSelectedCalendars(cals.flat());
}}
>
<Card
style={{
padding: 16,
borderRadius: 12,
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
display: "flex",
flexDirection: "column",
maxHeight: "80vh",
}}
>
<Box style={{ position: "absolute", top: 8, right: 8 }}>
<IconButton
size="small"
onClick={() => onClose({}, "backdropClick")}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
/>
<CardHeader
title="Browse other calendars"
style={{ paddingBottom: 0 }}
/>
<CardContent style={{ flex: 1, overflow: "auto" }}>
<PeopleSearch
objectTypes={["user"]}
selectedUsers={selectedUsers}
onChange={async (event: any, value: User[]) => {
setSelectedUsers(value);
const cals = await Promise.all(
value.map(async (user: User) => {
const cals = (await getCalendars(
user.openpaasId,
"sharedPublic=true&withRights=true"
)) as Record<string, any>;
return cals._embedded?.["dav:calendar"]
? cals._embedded["dav:calendar"].map(
(cal: Record<string, any>) => ({ cal, owner: user })
)
: { cal: undefined, owner: user };
})
);
setSelectedCalendars(cals.flat());
}}
/>
<SelectedCalendarsList
calendars={calendars}
selectedCal={selectedCal}
onRemove={(cal) => {
setSelectedCalendars((prev) =>
prev.filter(
(c) => c.cal._links.self.href !== cal.cal._links.self.href
)
);
if (
!selectedCal.find(
(c) =>
cal.owner.email === c.owner.email &&
c.cal._links.self.href !== cal.cal._links.self.href
)
) {
setSelectedUsers((prev) =>
prev.filter((u) => u.email !== cal.owner.email)
);
}
}}
onColorChange={(cal, color) =>
setSelectedCalendars((prev) =>
prev.map((prevcal) =>
prevcal.owner.email === cal.owner.email &&
prevcal.cal._links.self.href === cal.cal._links.self.href
? {
...prevcal,
cal: { ...prevcal.cal, "apple:color": color },
}
: prevcal
)
)
}
/>
</CardContent>
<CardActions style={{ justifyContent: "flex-end", gap: 8 }}>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button variant="contained" onClick={handleSave}>
Add
</Button>
</CardActions>
</Card>
</Box>
</Modal>
<SelectedCalendarsList
calendars={calendars}
selectedCal={selectedCal}
onRemove={(cal) => {
setSelectedCalendars((prev) =>
prev.filter(
(c) => c.cal._links.self.href !== cal.cal._links.self.href
)
);
if (
!selectedCal.find(
(c) =>
cal.owner.email === c.owner.email &&
c.cal._links.self.href !== cal.cal._links.self.href
)
) {
setSelectedUsers((prev) =>
prev.filter((u) => u.email !== cal.owner.email)
);
}
}}
onColorChange={(cal, color) =>
setSelectedCalendars((prev) =>
prev.map((prevcal) =>
prevcal.owner.email === cal.owner.email &&
prevcal.cal._links.self.href === cal.cal._links.self.href
? {
...prevcal,
cal: {
...prevcal.cal,
"apple:color": color.light,
"X-TWAKE-Dark-theme-color": color.dark,
},
}
: prevcal
)
)
}
/>
</ResponsiveDialog>
);
}
+10 -3
View File
@@ -218,8 +218,8 @@ function CalendarSelector({
<label>
<Checkbox
sx={{
color: calendars[id].color,
"&.Mui-checked": { color: calendars[id].color },
color: calendars[id].color?.light,
"&.Mui-checked": { color: calendars[id].color?.light },
}}
size="small"
checked={selectedCalendars.includes(id)}
@@ -232,7 +232,14 @@ function CalendarSelector({
</IconButton>
</div>
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => setOpen()}>Modify</MenuItem>
<MenuItem
onClick={() => {
setOpen();
handleClose();
}}
>
Modify
</MenuItem>
{!isDefault && <Divider />}
{!isDefault && (
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
+1 -1
View File
@@ -30,7 +30,7 @@ export function ImportTab({
setName: Function;
description: string;
setDescription: Function;
color: string;
color: Record<string, string>;
setColor: Function;
visibility: "public" | "private";
setVisibility: Function;
+1 -1
View File
@@ -29,7 +29,7 @@ export function SettingsTab({
setName: Function;
description: string;
setDescription: Function;
color: string;
color: Record<string, string>;
setColor: Function;
visibility: "public" | "private";
setVisibility: Function;
@@ -1,3 +1,4 @@
import { useTheme } from "@mui/material/styles";
import { useRef } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
@@ -6,6 +7,7 @@ import {
} from "../../features/Calendars/CalendarSlice";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
const requestControllers = new Map<string, AbortController>();
@@ -26,6 +28,7 @@ export function TempCalendarsInput({
const tempcalendars =
useAppSelector((state) => state.calendars.templist) ?? {};
const calendars = useAppSelector((state) => state.calendars.list);
const theme = useTheme();
const prevUsersRef = useRef<User[]>([]);
const userColors = new Map<string, string>();
@@ -59,8 +62,12 @@ export function TempCalendarsInput({
const existingColors = new Set(userColors.values());
userColors.set(user.email, generateUserColor(existingColors));
}
const lightColor = userColors.get(user.email)!;
user.color = userColors.get(user.email)!;
user.color = {
light: lightColor,
dark: getAccessiblePair(lightColor, theme),
};
dispatch(
getTempCalendarsListAsync(user, { signal: controller.signal })
@@ -127,7 +127,9 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
);
const delegated = calendar.delegated;
let Icon = null;
let titleStyle: React.CSSProperties = {};
let titleStyle: React.CSSProperties = {
color: event._def.extendedProps.colors?.dark,
};
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
ownerEmails.has(att.cal_address.toLowerCase())
@@ -265,7 +267,7 @@ function RenderEventTitle(
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: calendar.color,
backgroundColor: calendar.color?.light,
color: "white",
borderRadius: "4px",
border: "1px",
@@ -0,0 +1,55 @@
import { darken, getContrastRatio, lighten, Theme } from "@mui/material";
import { ThunkDispatch } from "@reduxjs/toolkit";
import { updateCalColor } from "../../../features/Calendars/CalendarSlice";
import { Calendars } from "../../../features/Calendars/CalendarTypes";
export function updateDarkColor(
calendars: Record<string, Calendars>,
theme: Theme,
dispatch: ThunkDispatch<any, any, any>
) {
Object.values(calendars).forEach((cal) => {
if (!cal?.color?.light) return;
const baseColor = cal.color.light;
const isDefault = Object.values(defaultColors).find(
(c) => c.light === baseColor
);
const darkColor = isDefault
? isDefault.dark
: getAccessiblePair(baseColor, theme);
dispatch(
updateCalColor({
id: cal.id,
color: { light: baseColor, dark: darkColor },
})
);
});
}
export function getAccessiblePair(baseColor: string, theme: Theme): string {
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);
console.log(isLight, contrast);
if (contrast >= 4.5) return adjusted;
if (isLight) {
return "#ffffffff";
}
return theme.palette.getContrastText(baseColor);
}
export const defaultColors = [
{ light: "#D0ECDA", dark: "#329655" },
{ light: "#FAE3CE", dark: "#E15300" },
{ light: "#F5CFD0", dark: "#BE0103" },
{ light: "#AFCBEF", dark: "#0654B1" },
];
@@ -62,9 +62,9 @@ export const eventToFullCalendarFormat = (
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
.map((e) => {
if (e.calId.split("/")[0] === userId) {
return { ...e, editable: true };
return { ...e, colors: e.color, color: e.color?.light, editable: true };
}
return { ...e, editable: false };
return { ...e, colors: e.color, color: e.color?.light, editable: false };
});
};
+8 -5
View File
@@ -34,7 +34,7 @@ export async function getCalendar(
export async function postCalendar(
userId: string,
calId: string,
color: string,
color: Record<string, string>,
name: string,
desc: string
) {
@@ -67,7 +67,10 @@ export async function addSharedCalendar(
"calendarserver:source": {
acl: cal.cal.acl,
calendarHomeId: cal.cal.id,
color: cal.cal["apple:color"],
color: {
light: cal.cal["apple:color"],
dark: cal.cal["X-TWAKE-Dark-theme-color"],
},
description: cal.cal["caldav:description"],
href: cal.cal._links.self.href,
id: cal.cal.id,
@@ -81,7 +84,7 @@ export async function addSharedCalendar(
export async function proppatchCalendar(
calLink: string,
patch: { name: string; desc: string; color: string }
patch: { name: string; desc: string; color: Record<string, string> }
) {
const body: Record<string, string> = {};
if (patch.name) {
@@ -90,8 +93,8 @@ export async function proppatchCalendar(
if (patch.desc) {
body["caldav:description"] = patch.desc;
}
if (patch.color) {
body["apple:color"] = patch.color;
if (patch.color.light) {
body["apple:color"] = patch.color.light;
}
const response = await api(`dav${calLink}`, {
method: "PROPPATCH",
+42 -14
View File
@@ -53,7 +53,10 @@ export const getCalendarsListAsync = createAsyncThunk<
const id = source.replace("/calendars/", "").replace(".json", "");
const visibility = getCalendarVisibility(cal["acl"]);
const ownerData: any = await getUserDetails(id.split("/")[0]);
const color = cal["apple:color"];
const color = {
light: cal["apple:color"] ?? "#006BD8",
dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
};
importedCalendars[id] = {
id,
name,
@@ -106,7 +109,10 @@ export const getTempCalendarsListAsync = createAsyncThunk<
ownerEmails: ownerData.emails,
description,
delegated,
color: tempUser.color ?? "#a8a8a8ff",
color: {
light: tempUser.color?.light ?? "#a8a8a8ff",
dark: tempUser.color?.dark ?? "#a8a8a8ff",
},
visibility,
events: {},
};
@@ -157,7 +163,7 @@ export const putEventAsync = createAsyncThunk<
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(
vevent[1],
cal.color ?? "",
cal.color ?? {},
cal.id,
eventURL,
valarm
@@ -187,12 +193,12 @@ export const patchCalendarAsync = createAsyncThunk<
{
calId: string;
calLink: string;
patch: { name: string; desc: string; color: string };
patch: { name: string; desc: string; color: Record<string, string> };
}, // Return type
{
calId: string;
calLink: string;
patch: { name: string; desc: string; color: string };
patch: { name: string; desc: string; color: Record<string, string> };
} // Arg type
>("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
await proppatchCalendar(calLink, patch);
@@ -238,7 +244,7 @@ export const moveEventAsync = createAsyncThunk<
const vevents = eventdata.data[2] as any[][];
const eventURL = eventdata._links.self.href;
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id, eventURL);
return parseCalendarEvent(vevent[1], cal.color ?? {}, cal.id, eventURL);
});
}
);
@@ -304,13 +310,19 @@ export const createCalendarAsync = createAsyncThunk<
{
userId: string;
calId: string;
color: string;
color: Record<string, string>;
name: string;
desc: string;
owner: string;
ownerEmails: string[];
}, // Return type
{ userId: string; calId: string; color: string; name: string; desc: string } // Arg type
{
userId: string;
calId: string;
color: Record<string, string>;
name: string;
desc: string;
} // Arg type
>("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
await postCalendar(userId, calId, color, name, desc);
const ownerData: any = await getUserDetails(userId.split("/")[0]);
@@ -331,7 +343,7 @@ export const createCalendarAsync = createAsyncThunk<
export const addSharedCalendarAsync = createAsyncThunk<
{
calId: string;
color: string;
color: Record<string, string>;
link: string;
name: string;
desc: string;
@@ -352,7 +364,10 @@ export const addSharedCalendarAsync = createAsyncThunk<
calId: cal.cal._links.self.href
.replace("/calendars/", "")
.replace(".json", ""),
color: cal.cal["apple:color"],
color: {
light: cal.cal["apple:color"],
dark: "#000",
},
link: `/calendars/${userId}/${calId}.json`,
desc: cal.cal["caldav:description"],
name: cal.cal["dav:name"],
@@ -388,12 +403,15 @@ const CalendarSlice = createSlice({
timeZone: string;
},
reducers: {
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
createCalendar: (
state,
action: PayloadAction<Record<string, string | Record<string, string>>>
) => {
const id = Date.now().toString(36);
state.list[id] = {} as Calendars;
state.list[id].name = action.payload.name;
state.list[id].color = action.payload.color;
state.list[id].description = action.payload.description;
state.list[id].name = action.payload.name as string;
state.list[id].color = action.payload.color as Record<string, string>;
state.list[id].description = action.payload.description as string;
state.list[id].events = {};
},
addEvent: (
@@ -450,6 +468,15 @@ const CalendarSlice = createSlice({
if (!state.list[action.payload]) return;
state.list[action.payload].lastCacheCleared = Date.now();
},
updateCalColor: (
state,
action: PayloadAction<{
id: string;
color: { light: string; dark: string };
}>
) => {
state.list[action.payload.id].color = action.payload.color;
},
},
extraReducers: (builder) => {
builder
@@ -719,5 +746,6 @@ export const {
emptyEventsCal,
setTimeZone,
clearFetchCache,
updateCalColor,
} = CalendarSlice.actions;
export default CalendarSlice.reducer;
+1 -1
View File
@@ -6,7 +6,7 @@ export interface Calendars {
name: string;
delegated?: boolean;
prodid?: string;
color?: string;
color?: Record<string, string>;
ownerEmails?: string[];
owner: string;
description?: string;
+1 -1
View File
@@ -36,7 +36,7 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
const eventjson = parseCalendarEvent(
targetVevent[1],
event.color ?? "",
event.color ?? {},
event.calId,
event.URL
);
+4 -2
View File
@@ -48,6 +48,7 @@ import { userAttendee } from "../User/userDataTypes";
import { getEvent } from "./EventApi";
import { formatLocalDateTime } from "../../components/Event/EventFormFields";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { light } from "@mui/material/styles/createPalette";
export default function EventDisplayModal({
eventId,
@@ -224,7 +225,8 @@ export default function EventDisplayModal({
<Typography variant="body2">
<CircleIcon
style={{
color: userPersonnalCalendars[index].color ?? "#3788D8",
color:
userPersonnalCalendars[index].color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
@@ -238,7 +240,7 @@ export default function EventDisplayModal({
<Typography variant="body2">
<CircleIcon
style={{
color: calendars[index].color ?? "#3788D8",
color: calendars[index].color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
+1 -1
View File
@@ -577,7 +577,7 @@ export default function EventPreviewModal({
<CalendarTodayIcon style={{ fontSize: 16 }} />
<CircleIcon
style={{
color: calendar.color ?? "#3788D8",
color: calendar.color?.light ?? "#3788D8",
width: 12,
height: 12,
}}
+1 -1
View File
@@ -17,7 +17,7 @@ export interface CalendarEvent {
attendee: userAttendee[];
stamp?: string;
sequence?: Number;
color?: string;
color?: Record<string, string>;
allday?: boolean;
error?: string;
status?: string;
+2 -2
View File
@@ -1,5 +1,5 @@
import { userAttendee } from "../User/userDataTypes";
import { AlarmObject, CalendarEvent } from "./EventsTypes";
import { AlarmObject, CalendarEvent, RepetitionObject } from "./EventsTypes";
import ICAL from "ical.js";
import { TIMEZONES } from "../../utils/timezone-data";
import moment from "moment";
@@ -7,7 +7,7 @@ type RawEntry = [string, Record<string, string>, string, any];
export function parseCalendarEvent(
data: RawEntry[],
color: string,
color: Record<string, string>,
calendarid: string,
eventURL: string,
valarm?: RawEntry[]