[#189] added custom event chip (#262)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-10-31 11:50:22 +01:00
committed by GitHub
parent 39d8085766
commit ec7230fa94
14 changed files with 689 additions and 218 deletions
+4 -4
View File
@@ -40,7 +40,7 @@ describe("CalendarSelection", () => {
"user1/cal1": {
name: "Calendar personnal",
id: "user1/cal1",
color: "#FF0000",
color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -72,7 +72,7 @@ describe("CalendarSelection", () => {
name: "Calendar delegated",
delegated: true,
id: "user2/cal1",
color: "#FF0000",
color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -103,7 +103,7 @@ describe("CalendarSelection", () => {
"user3/cal1": {
name: "Calendar shared",
id: "user3/cal1",
color: "#FF0000",
color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -292,7 +292,7 @@ describe("calendar Availability search", () => {
"user1/cal1": {
name: "Calendar personnal",
id: "user1/cal1",
color: "#FF0000",
color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {},
},
+24 -19
View File
@@ -36,7 +36,7 @@ describe("CalendarApp integration", () => {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
color: { light: "#FFFFFF", dark: "#000000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -123,7 +123,7 @@ describe("CalendarApp integration", () => {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
color: { light: "#FFFFFF", dark: "#000000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -157,6 +157,10 @@ describe("CalendarApp integration", () => {
});
it("renders lock icon for private events on the calendar", async () => {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
value: 120,
});
const preloadedState = createPreloadedState({
class: "PRIVATE",
title: "Private Event",
@@ -166,15 +170,16 @@ describe("CalendarApp integration", () => {
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
const eventEls = screen.getAllByText("Private Event");
const found = eventEls.some((eventEl) =>
within(eventEl.parentElement as HTMLElement).queryByTestId("lock-icon")
);
expect(found).toBe(true);
const card = screen.getByTestId("event-card-event1");
const lockIcon = within(card).getByTestId("LockOutlineIcon");
expect(lockIcon).toBeInTheDocument();
});
it("renders lock icon for confidential events on the calendar", async () => {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
value: 120,
});
const preloadedState = createPreloadedState({
class: "CONFIDENTIAL",
title: "Confidential Event",
@@ -185,14 +190,16 @@ describe("CalendarApp integration", () => {
preloadedState
);
const eventEls = screen.getAllByText("Confidential Event");
const found = eventEls.some((eventEl) =>
within(eventEl.parentElement as HTMLElement).queryByTestId("lock-icon")
);
expect(found).toBe(true);
const card = screen.getByTestId("event-card-event1");
const lockIcon = within(card).getByTestId("LockOutlineIcon");
expect(lockIcon).toBeInTheDocument();
});
it("does NOT render a lock icon for public events on the calendar", async () => {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
value: 120,
});
const preloadedState = createPreloadedState({
class: "PUBLIC",
title: "Public Event",
@@ -203,11 +210,9 @@ describe("CalendarApp integration", () => {
preloadedState
);
const eventEls = screen.getAllByText("Public Event");
const found = eventEls.some((eventEl) =>
within(eventEl.parentElement as HTMLElement).queryByTestId("lock-icon")
);
expect(found).toBe(false);
const card = screen.getByTestId("event-card-event1");
const lockIcon = within(card).queryByTestId("LockOutlineIcon");
expect(lockIcon).not.toBeInTheDocument();
});
it("does render a title for events without any attendees or user as organizer", async () => {
@@ -229,7 +234,7 @@ describe("CalendarApp integration", () => {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -28,7 +28,7 @@ describe("MiniCalendar", () => {
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
color: "#FF0000",
color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["test@test.com"],
events: {
event1: {
+5 -12
View File
@@ -50,19 +50,12 @@
.sidebar:hover
scrollbar-color: rgba(0, 0, 0, 0.5) transparent
.declined-event
opacity 0.7
// .declined-event
// opacity 0.7
.needs-action-event
opacity 0.7
border dashed 1px #ffffff
.fc-event-main span,
.fc-daygrid-event div
display block !important
white-space nowrap !important
overflow hidden !important
text-overflow ellipsis !important
// .needs-action-event
// opacity 0.7
// border dashed 1px #ffffff
[data-theme="dark"] .fc-timegrid-slot::after
border-top 1px dotted rgba(255, 255, 255, 0.2)
+58 -39
View File
@@ -251,37 +251,26 @@ tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller
/* FullCalendar "more" popover restyle */
.fc-daygrid-day-events
display: flex
flex-direction: column
height 70%
.fc-daygrid-day-bottom
position absolute
bottom 0
font-weight 500
font-size 12px
line-height 16px
letter-spacing 0.5px
color #6D7885
.fc-more-popover
z-index 700 !important
border-radius 4px
border-radius 8px
background #fff
overflow hidden
font-family Inter
font-family Inter, sans-serif
width 244px
box-shadow 0 4px 16px rgba(0,0,0,0.1)
.fc-popover-header
background #FCFCFC
display flex
justify-content space-between
align-items center
width 244px
height 40px
padding 12px
border-bottom 1px solid #eee
.fc-popover-title
font-family Inter
font-weight 500
font-size 14px
color #243B55
@@ -290,36 +279,66 @@ tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller
.fc-popover-close
color #605D62
cursor pointer
.fc-popover-body
max-height 208px
overflow auto
padding 0 !important
background #fff
.MuiCard-root
box-shadow none !important
border none !important
border-radius 8px
background-color transparent !important
margin 2px 6px
transition background 0.15s ease
&:hover
background #f7f7f8
.MuiCardHeader-root
padding 4px 8px
.MuiTypography-root
white-space nowrap
overflow hidden
text-overflow ellipsis
font-family: Inter !important
font-weight: 400 !important
font-style: Regular !important
font-size: 14px !important
line-height: 20px !important
letter-spacing: 0.25px !important
vertical-align: middle !important
color #1C1B1F !important
.compactStartTime
display none
.MuiCardContent-root
padding 0 8px 4px 8px
font-size 12px
color #6D7885
line-height 16px
/* Hide FullCalendars extra margin block */
.fc-more-popover-misc
height 0 !important
margin 0 !important
padding 0 !important
.fc-daygrid-event
&:hover
background #f7f7f8
div
display flex
align-items center
padding 4px 6px 4px 12px
border-radius 8px
color #1C1B1F!important
background-color transparent!important
border none
color #334155
font-size 14px
font-weight 400
line-height 20px
letter-spacing 0.25px
vertical-align middle
transition background 0.15s ease, color 0.15s ease
span
white-space nowrap
overflow hidden
text-overflow ellipsis
// Custom event chips
.fc-event
background-color transparent
border none
width 95%
height 100%
align-content left
.fc-event-main
display flex
flex-direction row
align-items center
.fc-timegrid-event-harness-inset .fc-timegrid-event
box-shadow none
@@ -1,13 +1,10 @@
import React from "react";
import { CalendarApi, NowIndicatorContentArg } from "@fullcalendar/core";
import { updateSlotLabelVisibility } from "../utils/calendarUtils";
import { createMouseHandlers } from "./mouseHandlers";
import { userAttendee } from "../../../features/User/userDataTypes";
import { Calendars } from "../../../features/Calendars/CalendarTypes";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import AccessTimeIcon from "@mui/icons-material/AccessTime";
import LockIcon from "@mui/icons-material/Lock";
import { EventErrorHandler } from "../../Error/EventErrorHandler";
import { EventChip } from "../../Event/EventChip/EventChip";
export interface ViewHandlersProps {
calendarRef: React.RefObject<CalendarApi | null>;
@@ -108,79 +105,12 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
};
const handleEventContent = (arg: any) => {
const event = arg.event;
const props = event._def.extendedProps;
const {
calId,
temp,
attendee: attendees = [],
class: classification,
} = props;
try {
const calendarsSource = temp ? tempcalendars : calendars;
const calendar = calendarsSource[calId];
if (!calendar) return null;
const isPrivate = ["PRIVATE", "CONFIDENTIAL"].includes(classification);
const ownerEmails = new Set(
calendar.ownerEmails?.map((e) => e.toLowerCase())
);
const delegated = calendar.delegated;
let Icon = null;
let titleStyle: React.CSSProperties = {
color: event._def.extendedProps.colors?.dark,
};
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
ownerEmails.has(att.cal_address.toLowerCase())
);
// if no special display
if (attendees.length && !delegated && !showSpecialDisplay.length) {
return React.createElement(
"div",
{ style: { display: "flex", alignItems: "center" } },
React.createElement("span", null, event.title)
);
}
switch (showSpecialDisplay?.[0]?.partstat) {
case "DECLINED":
Icon = null;
titleStyle.textDecoration = "line-through";
break;
case "TENTATIVE":
Icon = HelpOutlineIcon;
break;
case "NEEDS-ACTION":
Icon = AccessTimeIcon;
break;
case "ACCEPTED":
Icon = null;
break;
default:
break;
}
return RenderEventTitle(
titleStyle,
isPrivate,
Icon,
arg,
event,
calendar
);
} catch (e) {
const message =
e instanceof Error ? e.message : "Unknown error during rendering";
errorHandler.reportError(
event._def.extendedProps.uid || event.id,
message
);
return React.createElement(
"div",
{ style: { display: "flex", alignItems: "center" } },
React.createElement("span", null, event.title)
);
}
return React.createElement(EventChip, {
arg,
calendars,
tempcalendars,
errorHandler,
});
};
const handleEventDidMount = (arg: any) => {
@@ -228,65 +158,3 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
handleEventDidMount,
};
};
function RenderEventTitle(
titleStyle: React.CSSProperties,
isPrivate: boolean,
Icon: any,
arg: any,
event: any,
calendar: Calendars
): React.ReactNode {
const isMonthView = arg.view.type === "dayGridMonth";
const startTime = event._instance.range.start.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
});
const icons: React.ReactNode[] = [];
if (isPrivate) {
icons.push(
React.createElement(LockIcon, {
"data-testid": "lock-icon",
fontSize: "small",
style: { marginRight: "4px" },
})
);
}
if (Icon) {
icons.push(
React.createElement(Icon, {
fontSize: "small",
style: { marginRight: "4px" },
})
);
}
const containerStyle: React.CSSProperties = isMonthView
? {
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: calendar.color?.light,
color: "white",
borderRadius: "4px",
border: "1px",
width: "100%",
}
: {
display: "flex",
alignItems: "center",
};
const contentText =
isMonthView && !event._def.extendedProps.allday
? `${startTime} ${event.title}`
: event.title;
return React.createElement(
"div",
{ style: containerStyle },
...icons,
React.createElement("span", { style: titleStyle }, contentText)
);
}
@@ -62,9 +62,9 @@ export const eventToFullCalendarFormat = (
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
.map((e) => {
if (e.calId.split("/")[0] === userId) {
return { ...e, colors: e.color, color: e.color?.light, editable: true };
return { ...e, colors: e.color, editable: true };
}
return { ...e, colors: e.color, color: e.color?.light, editable: false };
return { ...e, colors: e.color, editable: false };
});
};
@@ -0,0 +1,31 @@
import React from "react";
import { Card, Typography } from "@mui/material";
import { EventErrorHandler } from "../../Error/EventErrorHandler";
export function ErrorEventChip({
event,
errorHandler,
error,
}: {
event: any;
errorHandler: EventErrorHandler;
error: any;
}) {
const message =
error instanceof Error ? error.message : "Unknown error during rendering";
errorHandler.reportError(event._def.extendedProps.uid || event.id, message);
return (
<Card
variant="outlined"
sx={{
px: 0.5,
py: 0.2,
borderRadius: "4px",
boxShadow: "none",
}}
>
<Typography variant="body2">{event.title}</Typography>
</Card>
);
}
@@ -0,0 +1,316 @@
import {
Avatar,
Box,
Card,
CardContent,
CardHeader,
Typography,
} from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
import { stringAvatar } from "../utils/eventUtils";
import { ErrorEventChip } from "./ErrorEventChip";
import {
DisplayedIcons,
EventChipProps,
getBestColor,
getCardStyle,
getEventDuration,
getEventTimes,
getOwnerAttendee,
getTitleStyle,
IconDisplayConfig,
} from "./EventChipUtils";
import { SimpleEventChip } from "./SimpleEventChip";
const COMPACT_WIDTH_THRESHOLD = 100;
const PRIVATE_CLASSIFICATIONS = ["PRIVATE", "CONFIDENTIAL"];
export const EVENT_DURATION = {
SHORT: 15,
MEDIUM: 30,
LONG: 60,
} as const;
function useCompactMode(
cardRef: React.RefObject<HTMLDivElement | null>
): boolean {
const [showCompact, setShowCompact] = useState(false);
useEffect(() => {
const checkWidth = () => {
if (cardRef.current) {
const width = cardRef.current.offsetWidth;
setShowCompact(width < COMPACT_WIDTH_THRESHOLD);
}
};
checkWidth();
const resizeObserver = new ResizeObserver(checkWidth);
if (cardRef.current) {
resizeObserver.observe(cardRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return showCompact;
}
export function EventChip({
arg,
calendars,
tempcalendars,
errorHandler,
}: EventChipProps) {
const cardRef = useRef<HTMLDivElement>(null);
const showCompact = useCompactMode(cardRef);
const event = arg.event;
const props = event._def.extendedProps;
const {
calId,
temp,
attendee: attendees = [],
class: classification,
} = props;
try {
// Calendar validation
const calendarsSource = temp ? tempcalendars : calendars;
const calendar = calendarsSource[calId];
if (!calendar) return null;
// Event properties
const isPrivate = PRIVATE_CLASSIFICATIONS.includes(classification);
const ownerEmails = new Set(
calendar.ownerEmails?.map((e) => e.toLowerCase())
);
const delegated = calendar.delegated;
// Determine owner attendee
const ownerAttendee = getOwnerAttendee(attendees, ownerEmails);
// Handle non-owner attendee case
if (attendees.length && !delegated && !ownerAttendee) {
return <SimpleEventChip title={event.title} />;
}
// Color and contrast logic
const bestColor = getBestColor(
(calendar.color as { light: string; dark: string }) ?? {
light: "#fff",
dark: "#000",
}
);
// Icon display configuration
const IconDisplayed: IconDisplayConfig = {
declined: ownerAttendee?.partstat === "DECLINED",
tentative: ownerAttendee?.partstat === "TENTATIVE",
needAction: ownerAttendee?.partstat === "NEEDS-ACTION",
private: isPrivate,
};
// View and time calculations
const isMonthView = arg.view.type === "dayGridMonth";
const timeZone = arg.view.calendar?.getOption("timeZone") || "UTC";
const { startTime, endTime } = getEventTimes(event, timeZone);
const eventLength = getEventDuration(event);
const isMoreThan15 = eventLength > EVENT_DURATION.SHORT;
const isMoreThan30 = eventLength > EVENT_DURATION.MEDIUM;
const isMoreThan60 = eventLength > EVENT_DURATION.LONG;
// Style calculation
const titleStyle = getTitleStyle(
bestColor,
ownerAttendee?.partstat,
calendar
);
const cardStyle = getCardStyle(
bestColor,
eventLength,
ownerAttendee?.partstat,
calendar
);
// Organizer avatar
const OrganizerAvatar = event._def.extendedProps.organizer
? stringAvatar(
event._def.extendedProps.organizer?.cn ??
event._def.extendedProps.organizer?.cal_address
)
: { style: {}, children: null };
return (
<Card
variant="outlined"
style={
!isMoreThan15 || isMonthView
? { ...cardStyle, height: "auto" }
: { ...cardStyle }
}
ref={cardRef}
data-testid={`event-card-${event._def.extendedProps.uid}`}
>
<CardHeader
sx={{
py: "0px",
px: "0px",
"& .MuiCardHeader-content": {
overflow: "hidden",
},
}}
title={
showCompact ? (
<Typography variant="body2" style={titleStyle}>
{event.title}
</Typography>
) : (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<Box
sx={{ display: "flex", alignItems: "center", minWidth: 0 }}
>
{(!isMoreThan30 || isMonthView) &&
!event._def.extendedProps.allday && (
<Typography
variant="body2"
className="compactStartTime"
style={{
...titleStyle,
textDecoration: "none",
overflow: "visible",
opacity: "70%",
letterSpacing: "0%",
fontSize: "10px",
marginRight: "4px",
}}
>
{startTime}
</Typography>
)}
{DisplayedIcons(IconDisplayed)}
<Typography variant="body2" noWrap style={titleStyle}>
{event.title}
</Typography>
</Box>
</Box>
)
}
subheader={
isMoreThan30 &&
!isMonthView &&
!event._def.extendedProps.allday && (
<Typography
style={{
color: titleStyle.color,
opacity: "70%",
fontFamily: "Inter",
fontWeight: "500",
fontSize: "10px",
lineHeight: "16px",
letterSpacing: "0%",
verticalAlign: "middle",
}}
>
{startTime} {!showCompact && ` - ${endTime}`}
</Typography>
)
}
/>
{isMoreThan60 &&
!showCompact &&
!isMonthView &&
!event._def.extendedProps.allday && (
<CardContent
sx={{
p: 0,
"& .MuiCardContent-content": {
overflow: "hidden",
},
}}
>
{event._def.extendedProps.location && (
<Typography
style={{
marginRight: 2,
fontFamily: "Roboto",
fontWeight: "500",
fontStyle: "Medium",
fontSize: "12px",
lineHeight: "16px",
letterSpacing: "0%",
verticalAlign: "middle",
color: titleStyle.color,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}}
>
{event._def.extendedProps.location}
</Typography>
)}
<Box sx={{ display: "flex", alignItems: "flex-start", mt: 0.5 }}>
{event._def.extendedProps.description && (
<Typography
sx={{
fontFamily: "Roboto",
fontWeight: 500,
fontSize: "10px",
lineHeight: "16px",
letterSpacing: "0%",
opacity: 0.8,
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
whiteSpace: "normal",
flex: 1,
maxWidth: "75%",
}}
>
{event._def.extendedProps.description}
</Typography>
)}
</Box>
</CardContent>
)}
{(isMoreThan60 || eventLength === 60) &&
!isMonthView &&
!event._def.extendedProps.allday &&
event._def.extendedProps.organizer &&
!showCompact &&
(window as any).displayOrgAvatar && (
<Avatar
children={OrganizerAvatar.children}
style={{
...OrganizerAvatar.style,
width: "20px",
height: "20px",
bottom: 0,
right: 0,
margin: "8px",
position: "absolute",
border: "2px solid white",
}}
/>
)}
</Card>
);
} catch (e) {
return (
<ErrorEventChip event={event} errorHandler={errorHandler} error={e} />
);
}
}
@@ -0,0 +1,182 @@
import CancelIcon from "@mui/icons-material/Cancel";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import LockOutlineIcon from "@mui/icons-material/LockOutline";
import { Box, getContrastRatio } from "@mui/material";
import moment from "moment";
import React from "react";
import { Calendars } from "../../../features/Calendars/CalendarTypes";
import { userAttendee } from "../../../features/User/userDataTypes";
import { EventErrorHandler } from "../../Error/EventErrorHandler";
import { EVENT_DURATION } from "./EventChip";
export interface EventChipProps {
arg: any;
calendars: Record<string, Calendars>;
tempcalendars: Record<string, Calendars>;
errorHandler: EventErrorHandler;
}
export interface IconDisplayConfig {
declined: boolean;
tentative: boolean;
needAction: boolean;
private: boolean;
}
export function getEventDuration(event: any): number {
return moment(event._instance.range.end).diff(
moment(event._instance.range.start),
"minutes"
);
}
export function getBestColor(colors: { light: string; dark: string }): string {
const contrastToDark = getContrastRatio(colors?.dark, "#fff");
const contrastToLight = getContrastRatio(colors?.light, "#fff");
return contrastToDark > contrastToLight ? colors?.dark : colors?.light;
}
export function getEventTimes(event: any, timeZone: string) {
return {
startTime: moment.tz(event.start, timeZone).format("HH:mm"),
endTime: moment.tz(event.end, timeZone).format("HH:mm"),
};
}
export function getOwnerAttendee(
attendees: userAttendee[],
ownerEmails: Set<string>
): userAttendee | undefined {
return attendees.find((att) =>
ownerEmails.has(att.cal_address.toLowerCase())
);
}
export function getTitleStyle(
bestColor: string,
partstat?: string,
calendar?: any
): React.CSSProperties {
const baseStyle: React.CSSProperties = {
fontFamily: "Roboto",
fontWeight: "500",
fontStyle: "Medium",
fontSize: "12px",
lineHeight: "16px",
letterSpacing: "0.5px",
verticalAlign: "middle",
overflow: "hidden",
textOverflow: "ellipsis",
font: "Roboto",
whiteSpace: "nowrap",
color: bestColor,
};
switch (partstat) {
case "DECLINED":
return { ...baseStyle, textDecoration: "line-through" };
case "TENTATIVE":
case "ACCEPTED":
return { ...baseStyle, color: calendar?.color?.dark };
case "NEEDS-ACTION":
return baseStyle;
default:
return baseStyle;
}
}
function getEventVariant(duration: number) {
if (duration <= EVENT_DURATION.SHORT) return "short";
if (duration <= EVENT_DURATION.MEDIUM) return "medium";
if (duration <= EVENT_DURATION.LONG) return "long";
return "extraLong";
}
function getCardVariantStyle(
variant: string,
baseColor: string
): React.CSSProperties {
const shared: React.CSSProperties = {
width: "100%",
height: "100%",
borderRadius: "8px",
boxShadow: "none",
border: `1px solid ${baseColor}`,
color: baseColor,
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
};
switch (variant) {
case "short":
return { ...shared, padding: "0px 6px", justifyContent: "center" };
case "medium":
return { ...shared, padding: "4px 6px", justifyContent: "center" };
default:
return {
...shared,
padding: "4px 6px",
};
}
}
export function getCardStyle(
bestColor: string,
eventLength: number,
partstat?: string,
calendar?: any
): React.CSSProperties {
const baseStyle: React.CSSProperties = getCardVariantStyle(
getEventVariant(eventLength),
bestColor
);
switch (partstat) {
case "DECLINED":
return baseStyle;
case "TENTATIVE":
return {
...baseStyle,
backgroundColor: calendar?.color?.light,
color: calendar?.color?.dark,
border: "1px solid white",
};
case "NEEDS-ACTION":
return {
...baseStyle,
backgroundColor: "#fff",
};
case "ACCEPTED":
return {
...baseStyle,
backgroundColor: calendar?.color?.light,
color: calendar?.color?.dark,
border: "1px solid white",
};
default:
return baseStyle;
}
}
export function DisplayedIcons(IconDisplayed: IconDisplayConfig) {
if (!Object.values(IconDisplayed).find((b) => b === true)) return;
return (
<Box
style={{
display: "flex",
flexDirection: "row",
gap: "1px",
marginRight: "4px",
}}
>
{IconDisplayed.needAction && (
<HelpOutlineIcon style={{ fontSize: "15px" }} />
)}
{IconDisplayed.declined && (
<CancelIcon color="error" style={{ fontSize: "15px" }} />
)}
{IconDisplayed.tentative && (
<HelpOutlineIcon style={{ fontSize: "15px" }} />
)}
{IconDisplayed.private && (
<LockOutlineIcon style={{ fontSize: "15px" }} />
)}
</Box>
);
}
@@ -0,0 +1,28 @@
import React from "react";
import { Card, Typography } from "@mui/material";
export function SimpleEventChip({ title }: { title: string }) {
return (
<Card
variant="outlined"
sx={{
borderRadius: "4px",
px: 0.5,
py: 0.2,
boxShadow: "none",
}}
>
<Typography
variant="body2"
sx={{
fontSize: "0.75rem",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{title}
</Typography>
</Card>
);
}
+3 -2
View File
@@ -7,6 +7,7 @@ type InfoRowProps = {
error?: boolean;
data?: string; // optional link target
content?: React.ReactNode; // if provided, overrides text rendering
style?: React.CSSProperties;
};
function detectUrls(text: string) {
@@ -60,6 +61,7 @@ export function InfoRow({
error = false,
data,
content,
style,
}: InfoRowProps) {
return (
<Box
@@ -83,8 +85,7 @@ export function InfoRow({
maxHeight: "33vh",
overflowY: "auto",
width: "100%",
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
...style,
}}
>
{data ? (
@@ -564,6 +564,10 @@ export default function EventPreviewModal({
</Box>
}
text={event.location}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
}}
/>
)}
{/* Description */}
@@ -575,6 +579,10 @@ export default function EventPreviewModal({
</Box>
}
text={event.description}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
}}
/>
)}
{/* ALARM */}
@@ -586,6 +594,10 @@ export default function EventPreviewModal({
</Box>
}
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
}}
/>
)}
{/* Repetition */}
@@ -597,6 +609,10 @@ export default function EventPreviewModal({
</Box>
}
text={makeRecurrenceString(event)}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
}}
/>
)}
</>
@@ -633,6 +649,10 @@ export default function EventPreviewModal({
</Box>
}
text={event.error}
style={{
fontSize: "16px",
fontFamily: "'Inter', sans-serif",
}}
error
/>
)}
+8
View File
@@ -43,6 +43,14 @@ jest.mock("openid-client", () => ({
}));
const originalWarn = console.warn;
class ResizeObserverMock {
observe() {}
unobserve() {}
disconnect() {}
}
(global as any).ResizeObserver = ResizeObserverMock;
beforeAll(() => {
console.warn = (...args: unknown[]) => {
if (