Color picker rework (#232)
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -82,7 +82,7 @@ describe("Calendar API", () => {
|
|||||||
it("patch Calendar", async () => {
|
it("patch Calendar", async () => {
|
||||||
const calId = "calId";
|
const calId = "calId";
|
||||||
const calLink = "/calendars/calId.json";
|
const calLink = "/calendars/calId.json";
|
||||||
const color = "calId";
|
const color = { light: "calIdLight", dark: "calIdDark" };
|
||||||
const name = "new cal";
|
const name = "new cal";
|
||||||
const desc = "desc";
|
const desc = "desc";
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ describe("Calendar API", () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"dav:name": "new cal",
|
"dav:name": "new cal",
|
||||||
"caldav:description": "desc",
|
"caldav:description": "desc",
|
||||||
"apple:color": "calId",
|
"apple:color": "calIdLight",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+11
@@ -31,6 +31,7 @@
|
|||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"openid-client": "^6.5.3",
|
"openid-client": "^6.5.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^13.5.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
@@ -11795,6 +11796,16 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"openid-client": "^6.5.3",
|
"openid-client": "^6.5.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^13.5.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import { searchUsers } from "../../features/User/userAPI";
|
|||||||
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
||||||
import Chip from "@mui/material/Chip";
|
import Chip from "@mui/material/Chip";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { getAccessiblePair } from "../Calendar/utils/calendarColorsUtils";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
email: string;
|
email: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
openpaasId: string;
|
openpaasId: string;
|
||||||
color?: string;
|
color?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PeopleSearch({
|
export function PeopleSearch({
|
||||||
@@ -150,8 +151,8 @@ export function PeopleSearch({
|
|||||||
const label = isString ? option : option.displayName || option.email;
|
const label = isString ? option : option.displayName || option.email;
|
||||||
const chipColor = isString
|
const chipColor = isString
|
||||||
? theme.palette.grey[300]
|
? theme.palette.grey[300]
|
||||||
: (option.color ?? theme.palette.grey[300]);
|
: (option.color?.light ?? theme.palette.grey[300]);
|
||||||
const textColor = theme.palette.getContrastText(chipColor);
|
const textColor = getAccessiblePair(chipColor, theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import momentTimezonePlugin from "@fullcalendar/moment-timezone";
|
|||||||
import { TimezoneSelector } from "./TimezoneSelector";
|
import { TimezoneSelector } from "./TimezoneSelector";
|
||||||
import { MiniCalendar } from "./MiniCalendar";
|
import { MiniCalendar } from "./MiniCalendar";
|
||||||
import { User } from "../Attendees/PeopleSearch";
|
import { User } from "../Attendees/PeopleSearch";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { updateDarkColor } from "./utils/calendarColorsUtils";
|
||||||
|
|
||||||
interface CalendarAppProps {
|
interface CalendarAppProps {
|
||||||
calendarRef: React.RefObject<CalendarApi | null>;
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
@@ -58,7 +60,7 @@ 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();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tokens || !userId) {
|
if (!tokens || !userId) {
|
||||||
dispatch(push("/"));
|
dispatch(push("/"));
|
||||||
@@ -108,6 +110,15 @@ export default function CalendarApp({
|
|||||||
}
|
}
|
||||||
}, [calendars, userId]);
|
}, [calendars, userId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateDarkColor(calendars, theme, dispatch);
|
||||||
|
}, [
|
||||||
|
theme,
|
||||||
|
Object.values(calendars)
|
||||||
|
.map((c) => c.color?.dark)
|
||||||
|
.join(","),
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validCalendarIds = new Set(Object.keys(calendars));
|
const validCalendarIds = new Set(Object.keys(calendars));
|
||||||
setSelectedCalendars((prev) =>
|
setSelectedCalendars((prev) =>
|
||||||
|
|||||||
@@ -1,36 +1,241 @@
|
|||||||
import { Box } from "@mui/material";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
const defaultColors = ["#34d399", "#fbbf24", "#f87171", "#60a5fa", "#f472b6"];
|
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({
|
export function ColorPicker({
|
||||||
selectedColor,
|
selectedColor,
|
||||||
colors = defaultColors,
|
colors = defaultColors,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
selectedColor?: string;
|
selectedColor: Record<string, string>;
|
||||||
colors?: string[];
|
colors?: Record<string, string>[];
|
||||||
onChange: (color: string) => void;
|
onChange: (color: Record<string, string>) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [customColor, setCustomColor] = useState(
|
||||||
|
!colors.find((c) => c.light === selectedColor?.light)
|
||||||
|
? selectedColor
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
{colors.map((c) => (
|
{colors.map((c) => (
|
||||||
<Box
|
<ColorBox
|
||||||
key={c}
|
key={c.light}
|
||||||
role="button"
|
color={c}
|
||||||
aria-label={`select color ${c}`}
|
onChange={onChange}
|
||||||
onClick={() => onChange(c)}
|
selectedColor={selectedColor}
|
||||||
style={{
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: c,
|
|
||||||
cursor: "pointer",
|
|
||||||
border:
|
|
||||||
selectedColor === c ? "2px solid black" : "2px solid transparent",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{customColor && (
|
||||||
|
<ColorBox
|
||||||
|
color={customColor ?? {}}
|
||||||
|
onChange={onChange}
|
||||||
|
selectedColor={selectedColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ColorPickerBox
|
||||||
|
onChange={(c) => {
|
||||||
|
onChange(c);
|
||||||
|
setCustomColor(c);
|
||||||
|
}}
|
||||||
|
selectedColor={selectedColor}
|
||||||
|
/>
|
||||||
</Box>
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ResponsiveDialog } from "../Dialog";
|
|||||||
import { AccessTab } from "./AccessTab";
|
import { AccessTab } from "./AccessTab";
|
||||||
import { ImportTab } from "./ImportTab";
|
import { ImportTab } from "./ImportTab";
|
||||||
import { SettingsTab } from "./SettingsTab";
|
import { SettingsTab } from "./SettingsTab";
|
||||||
|
import { defaultColors } from "./utils/calendarColorsUtils";
|
||||||
|
|
||||||
function CalendarPopover({
|
function CalendarPopover({
|
||||||
open,
|
open,
|
||||||
@@ -34,7 +35,7 @@ function CalendarPopover({
|
|||||||
// existing calendar params
|
// existing calendar params
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = 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");
|
const [visibility, setVisibility] = useState<"private" | "public">("public");
|
||||||
|
|
||||||
// import tab state
|
// import tab state
|
||||||
@@ -45,7 +46,7 @@ function CalendarPopover({
|
|||||||
// new calendar params (for import new)
|
// new calendar params (for import new)
|
||||||
const [newCalName, setNewCalName] = useState("");
|
const [newCalName, setNewCalName] = useState("");
|
||||||
const [newCalDescription, setNewCalDescription] = useState("");
|
const [newCalDescription, setNewCalDescription] = useState("");
|
||||||
const [newCalColor, setNewCalColor] = useState("");
|
const [newCalColor, setNewCalColor] = useState(defaultColors[0]);
|
||||||
const [newCalVisibility, setNewCalVisibility] = useState<
|
const [newCalVisibility, setNewCalVisibility] = useState<
|
||||||
"public" | "private"
|
"public" | "private"
|
||||||
>("public");
|
>("public");
|
||||||
@@ -55,13 +56,13 @@ function CalendarPopover({
|
|||||||
if (calendar) {
|
if (calendar) {
|
||||||
setName(calendar.name);
|
setName(calendar.name);
|
||||||
setDescription(calendar.description ?? "");
|
setDescription(calendar.description ?? "");
|
||||||
setColor(calendar.color ?? "");
|
setColor(calendar.color ?? defaultColors[0]);
|
||||||
setVisibility(calendar.visibility ?? "public");
|
setVisibility(calendar.visibility ?? "public");
|
||||||
setImportTarget(calendar.id ?? "new");
|
setImportTarget(calendar.id ?? "new");
|
||||||
} else {
|
} else {
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setColor("");
|
setColor(defaultColors[0]);
|
||||||
setVisibility("public");
|
setVisibility("public");
|
||||||
setImportTarget("new");
|
setImportTarget("new");
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ function CalendarPopover({
|
|||||||
calId: string,
|
calId: string,
|
||||||
name: string,
|
name: string,
|
||||||
desc: string,
|
desc: string,
|
||||||
color: string,
|
color: Record<string, string>,
|
||||||
visibility: string
|
visibility: string
|
||||||
) => {
|
) => {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
@@ -160,7 +161,7 @@ function CalendarPopover({
|
|||||||
onClose(e, reason);
|
onClose(e, reason);
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setColor("");
|
setColor(defaultColors[0]);
|
||||||
setTab("settings");
|
setTab("settings");
|
||||||
setVisibility("public");
|
setVisibility("public");
|
||||||
setImportTarget("new");
|
setImportTarget("new");
|
||||||
@@ -168,7 +169,7 @@ function CalendarPopover({
|
|||||||
|
|
||||||
setNewCalName("");
|
setNewCalName("");
|
||||||
setNewCalDescription("");
|
setNewCalDescription("");
|
||||||
setNewCalColor("");
|
setNewCalColor(defaultColors[0]);
|
||||||
setNewCalVisibility("public");
|
setNewCalVisibility("public");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
|
|||||||
import { ColorPicker } from "./CalendarColorPicker";
|
import { ColorPicker } from "./CalendarColorPicker";
|
||||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||||
|
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { ResponsiveDialog } from "../Dialog";
|
||||||
|
|
||||||
interface CalendarWithOwner {
|
interface CalendarWithOwner {
|
||||||
cal: Record<string, any>;
|
cal: Record<string, any>;
|
||||||
@@ -29,8 +32,10 @@ function CalendarItem({
|
|||||||
}: {
|
}: {
|
||||||
cal: CalendarWithOwner;
|
cal: CalendarWithOwner;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onColorChange: (color: string) => void;
|
onColorChange: (color: Record<string, string>) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={cal.owner.email + cal.cal["dav:name"]}
|
key={cal.owner.email + cal.cal["dav:name"]}
|
||||||
@@ -65,7 +70,10 @@ function CalendarItem({
|
|||||||
|
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
selectedColor={cal.cal["apple:color"]}
|
selectedColor={{
|
||||||
|
light: cal.cal["apple:color"],
|
||||||
|
dark: getAccessiblePair(cal.cal["apple:color"], theme),
|
||||||
|
}}
|
||||||
onChange={onColorChange}
|
onChange={onColorChange}
|
||||||
/>
|
/>
|
||||||
<IconButton size="small" onClick={onRemove}>
|
<IconButton size="small" onClick={onRemove}>
|
||||||
@@ -85,7 +93,10 @@ function SelectedCalendarsList({
|
|||||||
calendars: Record<string, Calendars>;
|
calendars: Record<string, Calendars>;
|
||||||
selectedCal: CalendarWithOwner[];
|
selectedCal: CalendarWithOwner[];
|
||||||
onRemove: (cal: CalendarWithOwner) => void;
|
onRemove: (cal: CalendarWithOwner) => void;
|
||||||
onColorChange: (cal: CalendarWithOwner, color: string) => void;
|
onColorChange: (
|
||||||
|
cal: CalendarWithOwner,
|
||||||
|
color: Record<string, string>
|
||||||
|
) => void;
|
||||||
}) {
|
}) {
|
||||||
if (selectedCal.length === 0) return null;
|
if (selectedCal.length === 0) return null;
|
||||||
|
|
||||||
@@ -194,7 +205,13 @@ export default function CalendarSearch({
|
|||||||
addSharedCalendarAsync({
|
addSharedCalendarAsync({
|
||||||
userId: openpaasId,
|
userId: openpaasId,
|
||||||
calId,
|
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
|
return cal.cal._links.self.href
|
||||||
@@ -212,122 +229,92 @@ export default function CalendarSearch({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ResponsiveDialog
|
||||||
open={open}
|
open={open}
|
||||||
|
contentSx={{ paddingTop: "8px !important" }}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
setSelectedCalendars([]);
|
setSelectedCalendars([]);
|
||||||
setSelectedUsers([]);
|
setSelectedUsers([]);
|
||||||
}}
|
}}
|
||||||
|
title="Browse other calendars"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onClose({}, "backdropClick")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleSave}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Box
|
<PeopleSearch
|
||||||
style={{
|
objectTypes={["user"]}
|
||||||
position: "absolute",
|
selectedUsers={selectedUsers}
|
||||||
left: "50%",
|
onChange={async (event: any, value: User[]) => {
|
||||||
top: "20%",
|
setSelectedUsers(value);
|
||||||
transform: "translate( -50%)",
|
|
||||||
width: "50vw",
|
const cals = await Promise.all(
|
||||||
maxHeight: "80vh",
|
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
|
<SelectedCalendarsList
|
||||||
title="Browse other calendars"
|
calendars={calendars}
|
||||||
style={{ paddingBottom: 0 }}
|
selectedCal={selectedCal}
|
||||||
/>
|
onRemove={(cal) => {
|
||||||
<CardContent style={{ flex: 1, overflow: "auto" }}>
|
setSelectedCalendars((prev) =>
|
||||||
<PeopleSearch
|
prev.filter(
|
||||||
objectTypes={["user"]}
|
(c) => c.cal._links.self.href !== cal.cal._links.self.href
|
||||||
selectedUsers={selectedUsers}
|
)
|
||||||
onChange={async (event: any, value: User[]) => {
|
);
|
||||||
setSelectedUsers(value);
|
if (
|
||||||
|
!selectedCal.find(
|
||||||
const cals = await Promise.all(
|
(c) =>
|
||||||
value.map(async (user: User) => {
|
cal.owner.email === c.owner.email &&
|
||||||
const cals = (await getCalendars(
|
c.cal._links.self.href !== cal.cal._links.self.href
|
||||||
user.openpaasId,
|
)
|
||||||
"sharedPublic=true&withRights=true"
|
) {
|
||||||
)) as Record<string, any>;
|
setSelectedUsers((prev) =>
|
||||||
return cals._embedded?.["dav:calendar"]
|
prev.filter((u) => u.email !== cal.owner.email)
|
||||||
? cals._embedded["dav:calendar"].map(
|
);
|
||||||
(cal: Record<string, any>) => ({ cal, owner: user })
|
}
|
||||||
)
|
}}
|
||||||
: { cal: undefined, owner: user };
|
onColorChange={(cal, color) =>
|
||||||
})
|
setSelectedCalendars((prev) =>
|
||||||
);
|
prev.map((prevcal) =>
|
||||||
|
prevcal.owner.email === cal.owner.email &&
|
||||||
setSelectedCalendars(cals.flat());
|
prevcal.cal._links.self.href === cal.cal._links.self.href
|
||||||
}}
|
? {
|
||||||
/>
|
...prevcal,
|
||||||
|
cal: {
|
||||||
<SelectedCalendarsList
|
...prevcal.cal,
|
||||||
calendars={calendars}
|
"apple:color": color.light,
|
||||||
selectedCal={selectedCal}
|
"X-TWAKE-Dark-theme-color": color.dark,
|
||||||
onRemove={(cal) => {
|
},
|
||||||
setSelectedCalendars((prev) =>
|
}
|
||||||
prev.filter(
|
: prevcal
|
||||||
(c) => c.cal._links.self.href !== cal.cal._links.self.href
|
)
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
if (
|
/>
|
||||||
!selectedCal.find(
|
</ResponsiveDialog>
|
||||||
(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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,8 +218,8 @@ function CalendarSelector({
|
|||||||
<label>
|
<label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
sx={{
|
sx={{
|
||||||
color: calendars[id].color,
|
color: calendars[id].color?.light,
|
||||||
"&.Mui-checked": { color: calendars[id].color },
|
"&.Mui-checked": { color: calendars[id].color?.light },
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
checked={selectedCalendars.includes(id)}
|
checked={selectedCalendars.includes(id)}
|
||||||
@@ -232,7 +232,14 @@ function CalendarSelector({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
|
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||||
<MenuItem onClick={() => setOpen()}>Modify</MenuItem>
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setOpen();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Modify
|
||||||
|
</MenuItem>
|
||||||
{!isDefault && <Divider />}
|
{!isDefault && <Divider />}
|
||||||
{!isDefault && (
|
{!isDefault && (
|
||||||
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
|
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function ImportTab({
|
|||||||
setName: Function;
|
setName: Function;
|
||||||
description: string;
|
description: string;
|
||||||
setDescription: Function;
|
setDescription: Function;
|
||||||
color: string;
|
color: Record<string, string>;
|
||||||
setColor: Function;
|
setColor: Function;
|
||||||
visibility: "public" | "private";
|
visibility: "public" | "private";
|
||||||
setVisibility: Function;
|
setVisibility: Function;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function SettingsTab({
|
|||||||
setName: Function;
|
setName: Function;
|
||||||
description: string;
|
description: string;
|
||||||
setDescription: Function;
|
setDescription: Function;
|
||||||
color: string;
|
color: Record<string, string>;
|
||||||
setColor: Function;
|
setColor: Function;
|
||||||
visibility: "public" | "private";
|
visibility: "public" | "private";
|
||||||
setVisibility: Function;
|
setVisibility: Function;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import {
|
import {
|
||||||
@@ -6,6 +7,7 @@ import {
|
|||||||
} from "../../features/Calendars/CalendarSlice";
|
} from "../../features/Calendars/CalendarSlice";
|
||||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||||
|
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||||
|
|
||||||
const requestControllers = new Map<string, AbortController>();
|
const requestControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ export function TempCalendarsInput({
|
|||||||
const tempcalendars =
|
const tempcalendars =
|
||||||
useAppSelector((state) => state.calendars.templist) ?? {};
|
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const prevUsersRef = useRef<User[]>([]);
|
const prevUsersRef = useRef<User[]>([]);
|
||||||
const userColors = new Map<string, string>();
|
const userColors = new Map<string, string>();
|
||||||
@@ -59,8 +62,12 @@ export function TempCalendarsInput({
|
|||||||
const existingColors = new Set(userColors.values());
|
const existingColors = new Set(userColors.values());
|
||||||
userColors.set(user.email, generateUserColor(existingColors));
|
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(
|
dispatch(
|
||||||
getTempCalendarsListAsync(user, { signal: controller.signal })
|
getTempCalendarsListAsync(user, { signal: controller.signal })
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
|||||||
);
|
);
|
||||||
const delegated = calendar.delegated;
|
const delegated = calendar.delegated;
|
||||||
let Icon = null;
|
let Icon = null;
|
||||||
let titleStyle: React.CSSProperties = {};
|
let titleStyle: React.CSSProperties = {
|
||||||
|
color: event._def.extendedProps.colors?.dark,
|
||||||
|
};
|
||||||
|
|
||||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||||
ownerEmails.has(att.cal_address.toLowerCase())
|
ownerEmails.has(att.cal_address.toLowerCase())
|
||||||
@@ -265,7 +267,7 @@ function RenderEventTitle(
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
backgroundColor: calendar.color,
|
backgroundColor: calendar.color?.light,
|
||||||
color: "white",
|
color: "white",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
border: "1px",
|
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 })))
|
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||||
.map((e) => {
|
.map((e) => {
|
||||||
if (e.calId.split("/")[0] === userId) {
|
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 };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function getCalendar(
|
|||||||
export async function postCalendar(
|
export async function postCalendar(
|
||||||
userId: string,
|
userId: string,
|
||||||
calId: string,
|
calId: string,
|
||||||
color: string,
|
color: Record<string, string>,
|
||||||
name: string,
|
name: string,
|
||||||
desc: string
|
desc: string
|
||||||
) {
|
) {
|
||||||
@@ -67,7 +67,10 @@ export async function addSharedCalendar(
|
|||||||
"calendarserver:source": {
|
"calendarserver:source": {
|
||||||
acl: cal.cal.acl,
|
acl: cal.cal.acl,
|
||||||
calendarHomeId: cal.cal.id,
|
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"],
|
description: cal.cal["caldav:description"],
|
||||||
href: cal.cal._links.self.href,
|
href: cal.cal._links.self.href,
|
||||||
id: cal.cal.id,
|
id: cal.cal.id,
|
||||||
@@ -81,7 +84,7 @@ export async function addSharedCalendar(
|
|||||||
|
|
||||||
export async function proppatchCalendar(
|
export async function proppatchCalendar(
|
||||||
calLink: string,
|
calLink: string,
|
||||||
patch: { name: string; desc: string; color: string }
|
patch: { name: string; desc: string; color: Record<string, string> }
|
||||||
) {
|
) {
|
||||||
const body: Record<string, string> = {};
|
const body: Record<string, string> = {};
|
||||||
if (patch.name) {
|
if (patch.name) {
|
||||||
@@ -90,8 +93,8 @@ export async function proppatchCalendar(
|
|||||||
if (patch.desc) {
|
if (patch.desc) {
|
||||||
body["caldav:description"] = patch.desc;
|
body["caldav:description"] = patch.desc;
|
||||||
}
|
}
|
||||||
if (patch.color) {
|
if (patch.color.light) {
|
||||||
body["apple:color"] = patch.color;
|
body["apple:color"] = patch.color.light;
|
||||||
}
|
}
|
||||||
const response = await api(`dav${calLink}`, {
|
const response = await api(`dav${calLink}`, {
|
||||||
method: "PROPPATCH",
|
method: "PROPPATCH",
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||||
const visibility = getCalendarVisibility(cal["acl"]);
|
const visibility = getCalendarVisibility(cal["acl"]);
|
||||||
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
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] = {
|
importedCalendars[id] = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -106,7 +109,10 @@ export const getTempCalendarsListAsync = createAsyncThunk<
|
|||||||
ownerEmails: ownerData.emails,
|
ownerEmails: ownerData.emails,
|
||||||
description,
|
description,
|
||||||
delegated,
|
delegated,
|
||||||
color: tempUser.color ?? "#a8a8a8ff",
|
color: {
|
||||||
|
light: tempUser.color?.light ?? "#a8a8a8ff",
|
||||||
|
dark: tempUser.color?.dark ?? "#a8a8a8ff",
|
||||||
|
},
|
||||||
visibility,
|
visibility,
|
||||||
events: {},
|
events: {},
|
||||||
};
|
};
|
||||||
@@ -157,7 +163,7 @@ export const putEventAsync = createAsyncThunk<
|
|||||||
return vevents.map((vevent: any[]) => {
|
return vevents.map((vevent: any[]) => {
|
||||||
return parseCalendarEvent(
|
return parseCalendarEvent(
|
||||||
vevent[1],
|
vevent[1],
|
||||||
cal.color ?? "",
|
cal.color ?? {},
|
||||||
cal.id,
|
cal.id,
|
||||||
eventURL,
|
eventURL,
|
||||||
valarm
|
valarm
|
||||||
@@ -187,12 +193,12 @@ export const patchCalendarAsync = createAsyncThunk<
|
|||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
patch: { name: string; desc: string; color: string };
|
patch: { name: string; desc: string; color: Record<string, string> };
|
||||||
}, // Return type
|
}, // Return type
|
||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
patch: { name: string; desc: string; color: string };
|
patch: { name: string; desc: string; color: Record<string, string> };
|
||||||
} // Arg type
|
} // Arg type
|
||||||
>("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
|
>("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
|
||||||
await proppatchCalendar(calLink, patch);
|
await proppatchCalendar(calLink, patch);
|
||||||
@@ -238,7 +244,7 @@ export const moveEventAsync = createAsyncThunk<
|
|||||||
const vevents = eventdata.data[2] as any[][];
|
const vevents = eventdata.data[2] as any[][];
|
||||||
const eventURL = eventdata._links.self.href;
|
const eventURL = eventdata._links.self.href;
|
||||||
return vevents.map((vevent: any[]) => {
|
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;
|
userId: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
color: string;
|
color: Record<string, string>;
|
||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
ownerEmails: string[];
|
ownerEmails: string[];
|
||||||
}, // Return type
|
}, // 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 }) => {
|
>("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
|
||||||
await postCalendar(userId, calId, color, name, desc);
|
await postCalendar(userId, calId, color, name, desc);
|
||||||
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
||||||
@@ -331,7 +343,7 @@ export const createCalendarAsync = createAsyncThunk<
|
|||||||
export const addSharedCalendarAsync = createAsyncThunk<
|
export const addSharedCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
color: string;
|
color: Record<string, string>;
|
||||||
link: string;
|
link: string;
|
||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
@@ -352,7 +364,10 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
|||||||
calId: cal.cal._links.self.href
|
calId: cal.cal._links.self.href
|
||||||
.replace("/calendars/", "")
|
.replace("/calendars/", "")
|
||||||
.replace(".json", ""),
|
.replace(".json", ""),
|
||||||
color: cal.cal["apple:color"],
|
color: {
|
||||||
|
light: cal.cal["apple:color"],
|
||||||
|
dark: "#000",
|
||||||
|
},
|
||||||
link: `/calendars/${userId}/${calId}.json`,
|
link: `/calendars/${userId}/${calId}.json`,
|
||||||
desc: cal.cal["caldav:description"],
|
desc: cal.cal["caldav:description"],
|
||||||
name: cal.cal["dav:name"],
|
name: cal.cal["dav:name"],
|
||||||
@@ -388,12 +403,15 @@ const CalendarSlice = createSlice({
|
|||||||
timeZone: string;
|
timeZone: string;
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
|
createCalendar: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<Record<string, string | Record<string, string>>>
|
||||||
|
) => {
|
||||||
const id = Date.now().toString(36);
|
const id = Date.now().toString(36);
|
||||||
state.list[id] = {} as Calendars;
|
state.list[id] = {} as Calendars;
|
||||||
state.list[id].name = action.payload.name;
|
state.list[id].name = action.payload.name as string;
|
||||||
state.list[id].color = action.payload.color;
|
state.list[id].color = action.payload.color as Record<string, string>;
|
||||||
state.list[id].description = action.payload.description;
|
state.list[id].description = action.payload.description as string;
|
||||||
state.list[id].events = {};
|
state.list[id].events = {};
|
||||||
},
|
},
|
||||||
addEvent: (
|
addEvent: (
|
||||||
@@ -450,6 +468,15 @@ const CalendarSlice = createSlice({
|
|||||||
if (!state.list[action.payload]) return;
|
if (!state.list[action.payload]) return;
|
||||||
state.list[action.payload].lastCacheCleared = Date.now();
|
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) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@@ -719,5 +746,6 @@ export const {
|
|||||||
emptyEventsCal,
|
emptyEventsCal,
|
||||||
setTimeZone,
|
setTimeZone,
|
||||||
clearFetchCache,
|
clearFetchCache,
|
||||||
|
updateCalColor,
|
||||||
} = CalendarSlice.actions;
|
} = CalendarSlice.actions;
|
||||||
export default CalendarSlice.reducer;
|
export default CalendarSlice.reducer;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface Calendars {
|
|||||||
name: string;
|
name: string;
|
||||||
delegated?: boolean;
|
delegated?: boolean;
|
||||||
prodid?: string;
|
prodid?: string;
|
||||||
color?: string;
|
color?: Record<string, string>;
|
||||||
ownerEmails?: string[];
|
ownerEmails?: string[];
|
||||||
owner: string;
|
owner: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
|||||||
|
|
||||||
const eventjson = parseCalendarEvent(
|
const eventjson = parseCalendarEvent(
|
||||||
targetVevent[1],
|
targetVevent[1],
|
||||||
event.color ?? "",
|
event.color ?? {},
|
||||||
event.calId,
|
event.calId,
|
||||||
event.URL
|
event.URL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { userAttendee } from "../User/userDataTypes";
|
|||||||
import { getEvent } from "./EventApi";
|
import { getEvent } from "./EventApi";
|
||||||
import { formatLocalDateTime } from "../../components/Event/EventFormFields";
|
import { formatLocalDateTime } from "../../components/Event/EventFormFields";
|
||||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
|
import { light } from "@mui/material/styles/createPalette";
|
||||||
|
|
||||||
export default function EventDisplayModal({
|
export default function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -224,7 +225,8 @@ export default function EventDisplayModal({
|
|||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
style={{
|
style={{
|
||||||
color: userPersonnalCalendars[index].color ?? "#3788D8",
|
color:
|
||||||
|
userPersonnalCalendars[index].color?.light ?? "#3788D8",
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
}}
|
}}
|
||||||
@@ -238,7 +240,7 @@ export default function EventDisplayModal({
|
|||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
style={{
|
style={{
|
||||||
color: calendars[index].color ?? "#3788D8",
|
color: calendars[index].color?.light ?? "#3788D8",
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ export default function EventPreviewModal({
|
|||||||
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
style={{
|
style={{
|
||||||
color: calendar.color ?? "#3788D8",
|
color: calendar.color?.light ?? "#3788D8",
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface CalendarEvent {
|
|||||||
attendee: userAttendee[];
|
attendee: userAttendee[];
|
||||||
stamp?: string;
|
stamp?: string;
|
||||||
sequence?: Number;
|
sequence?: Number;
|
||||||
color?: string;
|
color?: Record<string, string>;
|
||||||
allday?: boolean;
|
allday?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
import { AlarmObject, CalendarEvent } from "./EventsTypes";
|
import { AlarmObject, CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import ICAL from "ical.js";
|
import ICAL from "ical.js";
|
||||||
import { TIMEZONES } from "../../utils/timezone-data";
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -7,7 +7,7 @@ type RawEntry = [string, Record<string, string>, string, any];
|
|||||||
|
|
||||||
export function parseCalendarEvent(
|
export function parseCalendarEvent(
|
||||||
data: RawEntry[],
|
data: RawEntry[],
|
||||||
color: string,
|
color: Record<string, string>,
|
||||||
calendarid: string,
|
calendarid: string,
|
||||||
eventURL: string,
|
eventURL: string,
|
||||||
valarm?: RawEntry[]
|
valarm?: RawEntry[]
|
||||||
|
|||||||
Reference in New Issue
Block a user