Merge pull request #100 from linagora/92-manage-event-trnsparency

manage transparency
This commit is contained in:
Camille Moussu
2025-09-10 11:38:06 +02:00
committed by GitHub
5 changed files with 351 additions and 36 deletions
@@ -1045,6 +1045,296 @@ describe("Event Full Display", () => {
screen.getAllByDisplayValue(new RegExp(date, "i"))[0] screen.getAllByDisplayValue(new RegExp(date, "i"))[0]
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("saves event and moves it when calendar is changed", async () => {
const spyPut = jest
.spyOn(eventThunks, "putEventAsync")
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
const spyMove = jest
.spyOn(eventThunks, "moveEventAsync")
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
const spyRemove = jest.spyOn(eventThunks, "removeEvent");
const day = new Date();
const preloadedTwoCals = {
user: {
userData: {
sub: "test",
email: "test@test.com",
openpaasId: "667037022b752d0026472254",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
id: "667037022b752d0026472254/cal1",
name: "Calendar One",
color: "#FF0000",
events: {
event1: {
uid: "uid-base",
id: "event1",
title: "Test Event",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
end: day.toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
attendee: [{ cn: "test", cal_address: "test@test.com" }],
},
},
},
"667037022b752d0026472254/cal2": {
id: "667037022b752d0026472254/cal2",
name: "Calendar Two",
color: "#00FF00",
events: {},
},
},
pending: false,
},
};
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"event1"}
/>,
preloadedTwoCals
);
fireEvent.mouseDown(screen.getByLabelText("Calendar"));
const option = await screen.findByText("Calendar Two");
fireEvent.click(option);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(spyPut).toHaveBeenCalled();
});
await waitFor(() => {
expect(spyMove).toHaveBeenCalled();
});
expect(spyRemove).toHaveBeenCalled();
});
it("removes recurrence instances when saving an edited recurring series", async () => {
const spyPut = jest
.spyOn(eventThunks, "putEventAsync")
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
const spyRemove = jest.spyOn(eventThunks, "removeEvent");
const day = new Date();
const preloadedRecurrence = {
user: {
userData: {
sub: "test",
email: "test@test.com",
openpaasId: "667037022b752d0026472254",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
id: "667037022b752d0026472254/cal1",
name: "First Calendar",
color: "#FF0000",
events: {
"base/20250101": {
uid: "base/20250101",
calId: "667037022b752d0026472254/cal1",
title: "eventA",
},
"base/20250201": {
uid: "base/20250201",
calId: "667037022b752d0026472254/cal1",
title: "eventB",
},
"base/20250301": {
uid: "base/20250301",
title: "Recurring event",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
end: day.toISOString(),
organizer: { cal_address: "test@test.com" },
attendee: [{ cal_address: "test@test.com", cn: "Test" }],
},
},
},
},
pending: false,
},
};
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"base/20250301"}
/>,
preloadedRecurrence
);
act(() => fireEvent.click(screen.getByText("Save")));
await waitFor(() => {
expect(spyPut).toHaveBeenCalled();
});
await waitFor(() => {
expect(spyRemove).toHaveBeenCalled();
});
});
it("InfoRow renders error style when error prop is true", () => {
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
expect(screen.getByText("Bad")).toBeInTheDocument();
});
it("calls onClose from useEffect if event or calendar missing", () => {
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"nonexistent/cal"}
eventId={"ghost"}
/>,
preloadedState
);
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("renders error row when event has error", () => {
const errorState = {
...preloadedState,
calendars: {
list: {
"667037022b752d0026472254/cal1": {
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
events: {
event1: {
...preloadedState.calendars.list[
"667037022b752d0026472254/cal1"
].events.event1,
error: "Something went wrong",
},
},
},
},
pending: false,
},
};
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"event1"}
/>,
errorState
);
act(() => {
fireEvent.click(screen.getByText("Show More"));
});
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
});
it("can remove an attendee with the close button", () => {
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"event1"}
/>,
preloadedState
);
const removeBtn = screen.getAllByTestId("CloseIcon").pop()!;
fireEvent.click(removeBtn);
expect(screen.queryByText(/John/)).not.toBeInTheDocument();
});
it("shows more attendees when overflow, then toggles back", () => {
const overflowState = {
...preloadedState,
calendars: {
list: {
"667037022b752d0026472254/cal1": {
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
events: {
event1: {
...preloadedState.calendars.list[
"667037022b752d0026472254/cal1"
].events.event1,
attendee: new Array(6).fill(null).map((_, i) => ({
cn: `Person${i}`,
cal_address: `p${i}@test.com`,
})),
organizer: { cn: "test", cal_address: "test@test.com" },
},
},
},
},
},
};
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"event1"}
/>,
overflowState
);
const toggle = screen.getByText(/Show more/);
fireEvent.click(toggle);
expect(screen.getByText(/Show less/)).toBeInTheDocument();
});
it("renders video conference info when x_openpass_videoconference exists", () => {
const videoState = {
...preloadedState,
calendars: {
list: {
"667037022b752d0026472254/cal1": {
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
events: {
event1: {
...preloadedState.calendars.list[
"667037022b752d0026472254/cal1"
].events.event1,
x_openpass_videoconference: "https://meet.test/video",
},
},
},
},
},
};
renderWithProviders(
<EventDisplayModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"event1"}
/>,
videoState
);
expect(screen.getByText("Video conference available")).toBeInTheDocument();
});
}); });
describe("Helper functions", () => { describe("Helper functions", () => {
+1 -1
View File
@@ -247,7 +247,7 @@ describe("EventPopover", () => {
location: "Zoom", location: "Zoom",
organizer: { cn: "test", cal_address: "test@test.com" }, organizer: { cn: "test", cal_address: "test@test.com" },
timezone: "Europe/Paris", timezone: "Europe/Paris",
transp: "OPAQUE", transp: "TRANSPARENT",
}; };
// Fill inputs // Fill inputs
+45 -32
View File
@@ -89,13 +89,13 @@ export default function EventDisplayModal({
); );
const [allday, setAllDay] = useState(event?.allday); const [allday, setAllDay] = useState(event?.allday);
const [repetition, setRepetition] = useState<RepetitionObject>( const [repetition, setRepetition] = useState<RepetitionObject>(
event.repetition ?? ({} as RepetitionObject) event?.repetition ?? ({} as RepetitionObject)
); );
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [busy, setBusy] = useState(""); const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC"); const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
const [newCalId, setNewCalId] = useState(event.calId); const [newCalId, setNewCalId] = useState(event?.calId);
const [calendarid, setCalendarid] = useState( const [calendarid, setCalendarid] = useState(
calId.split("/")[0] === user.userData.openpaasId calId.split("/")[0] === user.userData.openpaasId
? userPersonnalCalendars.findIndex((cal) => cal.id === calId) ? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
@@ -112,7 +112,7 @@ export default function EventDisplayModal({
); );
const organizer = const organizer =
event.attendee?.find( event?.attendee?.find(
(a) => a.cal_address === event?.organizer?.cal_address (a) => a.cal_address === event?.organizer?.cal_address
) ?? ({} as userAttendee); ) ?? ({} as userAttendee);
@@ -124,7 +124,7 @@ export default function EventDisplayModal({
if (!event || !calendar) { if (!event || !calendar) {
onClose({}, "backdropClick"); onClose({}, "backdropClick");
} }
setRepetition(event.repetition ?? ({} as RepetitionObject)); setRepetition(event?.repetition ?? ({} as RepetitionObject));
}, [open, eventId, dispatch, onClose, event]); }, [open, eventId, dispatch, onClose, event]);
if (!event || !calendar) return null; if (!event || !calendar) return null;
@@ -158,7 +158,7 @@ export default function EventDisplayModal({
organizer: event.organizer, organizer: event.organizer,
timezone, timezone,
attendee: [organizer, ...attendees], attendee: [organizer, ...attendees],
transp: "OPAQUE", transp: busy,
color: userPersonnalCalendars[calendarid]?.color, color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" }, alarm: { trigger: alarm, action: "EMAIL" },
}; };
@@ -471,32 +471,32 @@ export default function EventDisplayModal({
setRepetition={setRepetition} setRepetition={setRepetition}
isOwn={isOwn} isOwn={isOwn}
/> />
<FormControl fullWidth margin="dense" size="small">
<FormControl fullWidth margin="dense" size="small"> <InputLabel id="alarm">Alarm</InputLabel>
<InputLabel id="alarm">Alarm</InputLabel> <Select
<Select labelId="alarm"
labelId="alarm" label="Alarm"
label="Alarm" value={alarm}
value={alarm} disabled={!isOwn}
disabled={!isOwn} onChange={(e: SelectChangeEvent) =>
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)} setAlarm(e.target.value)
> }
<MenuItem value={""}>No Alarm</MenuItem> >
<MenuItem value={"-PT1M"}>1 minute</MenuItem> <MenuItem value={""}>No Alarm</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem> <MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem> <MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem> <MenuItem value={"-PT10M"}>10 minutes</MenuItem>
<MenuItem value={"-PT30M"}>30 minutes</MenuItem> <MenuItem value={"-PT15M"}>15 minutes</MenuItem>
<MenuItem value={"-PT1H"}>1 hours</MenuItem> <MenuItem value={"-PT30M"}>30 minutes</MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem> <MenuItem value={"-PT1H"}>1 hours</MenuItem>
<MenuItem value={"-PT5H"}>5 hours</MenuItem> <MenuItem value={"-PT2H"}>2 hours</MenuItem>
<MenuItem value={"-PT12H"}>12 hours</MenuItem> <MenuItem value={"-PT5H"}>5 hours</MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem> <MenuItem value={"-PT12H"}>12 hours</MenuItem>
<MenuItem value={"-PT2D"}>2 days</MenuItem> <MenuItem value={"-PT1D"}>1 day</MenuItem>
<MenuItem value={"-PT1W"}>1 week</MenuItem> <MenuItem value={"-PT2D"}>2 days</MenuItem>
</Select> <MenuItem value={"-PT1W"}>1 week</MenuItem>
</FormControl> </Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="Visibility">Visibility</InputLabel> <InputLabel id="Visibility">Visibility</InputLabel>
<Select <Select
@@ -513,6 +513,19 @@ export default function EventDisplayModal({
<MenuItem value={"PRIVATE"}>Private</MenuItem> <MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="busy">is Busy</InputLabel>
<Select
labelId="busy"
value={busy}
disabled={!isOwn}
label="is busy"
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
>
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select>
</FormControl>
{/* Error */} {/* Error */}
{event.error && ( {event.error && (
<InfoRow <InfoRow
+14 -2
View File
@@ -76,7 +76,7 @@ function EventPopover({
const [attendees, setAttendees] = useState<userAttendee[]>([]); const [attendees, setAttendees] = useState<userAttendee[]>([]);
const [alarm, setAlarm] = useState(""); const [alarm, setAlarm] = useState("");
const [eventClass, setEventClass] = useState("PUBLIC"); const [eventClass, setEventClass] = useState("PUBLIC");
const [busy, setBusy] = useState(""); const [busy, setBusy] = useState("OPAQUE");
const [timezone, setTimezone] = useState( const [timezone, setTimezone] = useState(
Intl.DateTimeFormat().resolvedOptions().timeZone Intl.DateTimeFormat().resolvedOptions().timeZone
@@ -115,7 +115,7 @@ function EventPopover({
cutype: "INDIVIDUAL", cutype: "INDIVIDUAL",
}, },
], ],
transp: "OPAQUE", transp: busy,
color: userPersonnalCalendars[calendarid]?.color, color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" }, alarm: { trigger: alarm, action: "EMAIL" },
}; };
@@ -326,6 +326,18 @@ function EventPopover({
<MenuItem value={"PRIVATE"}>Private</MenuItem> <MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="busy">is Busy</InputLabel>
<Select
labelId="busy"
value={busy}
label="is busy"
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
>
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select>
</FormControl>
</> </>
)} )}
</CardContent> </CardContent>
+1 -1
View File
@@ -157,7 +157,7 @@ export function calendarEventToJCal(
["summary", {}, "text", event.title ?? ""], ["summary", {}, "text", event.title ?? ""],
], ],
]; ];
if (event.alarm) { if (event.alarm?.trigger) {
const valarm = [ const valarm = [
["trigger", {}, "duration", event.alarm.trigger], ["trigger", {}, "duration", event.alarm.trigger],
["action", {}, "text", event.alarm.action], ["action", {}, "text", event.alarm.action],