[#397 bis] added participation management on event from thunderbird (#432)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-01-07 11:45:17 +01:00
committed by GitHub
parent e59d878e6c
commit 9ff75056c7
47 changed files with 550 additions and 366 deletions
@@ -2,7 +2,7 @@ import { screen, fireEvent, waitFor } from "@testing-library/react";
import CalendarPopover from "../../../src/components/Calendar/CalendarModal"; import CalendarPopover from "../../../src/components/Calendar/CalendarModal";
import { renderWithProviders } from "../../utils/Renderwithproviders"; import { renderWithProviders } from "../../utils/Renderwithproviders";
import * as eventThunks from "../../../src/features/Calendars/CalendarSlice"; import * as eventThunks from "../../../src/features/Calendars/CalendarSlice";
import { Calendars } from "../../../src/features/Calendars/CalendarTypes"; import { Calendar } from "../../../src/features/Calendars/CalendarTypes";
import { getSecretLink } from "../../../src/features/Calendars/CalendarApi"; import { getSecretLink } from "../../../src/features/Calendars/CalendarApi";
jest.mock("../../../src/features/Calendars/CalendarApi", () => ({ jest.mock("../../../src/features/Calendars/CalendarApi", () => ({
@@ -103,7 +103,7 @@ describe("CalendarPopover (editing mode)", () => {
}, },
}; };
const existingCalendar: Calendars = { const existingCalendar: Calendar = {
id: "user1/cal1", id: "user1/cal1",
link: "/calendars/user/cal1", link: "/calendars/user/cal1",
name: "Work Calendar", name: "Work Calendar",
@@ -208,7 +208,7 @@ describe("CalendarPopover - Tabs Scenarios", () => {
}, },
}); });
const existingCalendar: Calendars = { const existingCalendar: Calendar = {
id: "user1/cal1", id: "user1/cal1",
link: "/calendars/user1/cal1.json", link: "/calendars/user1/cal1.json",
name: "Work Calendar", name: "Work Calendar",
@@ -22,7 +22,7 @@ import * as calAPI from "../../../src/features/Calendars/CalendarApi";
import * as userAPI from "../../../src/features/User/userAPI"; import * as userAPI from "../../../src/features/User/userAPI";
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import { Calendars } from "../../../src/features/Calendars/CalendarTypes"; import { Calendar } from "../../../src/features/Calendars/CalendarTypes";
import { CalendarEvent } from "../../../src/features/Events/EventsTypes"; import { CalendarEvent } from "../../../src/features/Events/EventsTypes";
jest.mock("../../../src/features/Calendars/CalendarApi"); jest.mock("../../../src/features/Calendars/CalendarApi");
@@ -79,7 +79,7 @@ describe("CalendarSlice", () => {
[calId]: { [calId]: {
id: calId, id: calId,
events: { e1: { uid: "e1" } }, events: { e1: { uid: "e1" } },
} as unknown as Calendars, } as unknown as Calendar,
}, },
}; };
const state = reducer( const state = reducer(
@@ -97,7 +97,7 @@ describe("CalendarSlice", () => {
[calId]: { [calId]: {
id: calId, id: calId,
events: { e1: { uid: "e1", title: "Old" } }, events: { e1: { uid: "e1", title: "Old" } },
} as unknown as Calendars, } as unknown as Calendar,
}, },
}; };
const state = reducer( const state = reducer(
@@ -276,7 +276,7 @@ describe("CalendarSlice", () => {
id: "u1/cal1", id: "u1/cal1",
name: "Existing Calendar", name: "Existing Calendar",
events: {}, events: {},
} as Calendars, } as Calendar,
}; };
const store = storeFactory(); const store = storeFactory();
@@ -447,7 +447,7 @@ describe("CalendarSlice", () => {
[calId]: { [calId]: {
id: calId, id: calId,
events: { e1: { uid: "e1" } }, events: { e1: { uid: "e1" } },
} as unknown as Calendars, } as unknown as Calendar,
}, },
}; };
const state = reducer( const state = reducer(
@@ -473,7 +473,7 @@ describe("CalendarSlice", () => {
ownerEmails: ["o@o.com"], ownerEmails: ["o@o.com"],
link: "/calendars/t1.json", link: "/calendars/t1.json",
description: "desc", description: "desc",
} as Calendars, } as Calendar,
}; };
const state = reducer( const state = reducer(
initialState, initialState,
@@ -533,7 +533,7 @@ describe("CalendarSlice", () => {
["c1"]: { ["c1"]: {
id: "c1", id: "c1",
events: {}, events: {},
} as unknown as Calendars, } as unknown as Calendar,
}, },
}, },
getCalendarDetailAsync.fulfilled(payload, "req11", { getCalendarDetailAsync.fulfilled(payload, "req11", {
+13 -11
View File
@@ -300,13 +300,13 @@ describe("Event Preview Display", () => {
screen.getByText("eventPreview.attendingQuestion") screen.getByText("eventPreview.attendingQuestion")
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "eventPreview.accept" }) screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "eventPreview.maybe" }) screen.getByRole("button", { name: "eventPreview.TENTATIVE" })
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "eventPreview.decline" }) screen.getByRole("button", { name: "eventPreview.DECLINED" })
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("doesnt renders RSVP buttons when user isnt an attendee", () => { it("doesnt renders RSVP buttons when user isnt an attendee", () => {
@@ -354,13 +354,13 @@ describe("Event Preview Display", () => {
screen.queryByText("eventPreview.attendingQuestion") screen.queryByText("eventPreview.attendingQuestion")
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect( expect(
screen.queryByRole("button", { name: "eventPreview.accept" }) screen.queryByRole("button", { name: "eventPreview.ACCEPTED" })
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect( expect(
screen.queryByRole("button", { name: "eventPreview.maybe" }) screen.queryByRole("button", { name: "eventPreview.TENTATIVE" })
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect( expect(
screen.queryByRole("button", { name: "eventPreview.decline" }) screen.queryByRole("button", { name: "eventPreview.DECLINED" })
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
@@ -414,7 +414,7 @@ describe("Event Preview Display", () => {
); );
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "eventPreview.accept" }) screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
); );
await waitFor(() => { await waitFor(() => {
@@ -474,7 +474,9 @@ describe("Event Preview Display", () => {
rsvpState rsvpState
); );
fireEvent.click(screen.getByRole("button", { name: "eventPreview.maybe" })); fireEvent.click(
screen.getByRole("button", { name: "eventPreview.TENTATIVE" })
);
await waitFor(() => { await waitFor(() => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@@ -534,7 +536,7 @@ describe("Event Preview Display", () => {
); );
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "eventPreview.decline" }) screen.getByRole("button", { name: "eventPreview.DECLINED" })
); );
await waitFor(() => { await waitFor(() => {
@@ -1020,7 +1022,7 @@ describe("Event Preview Display", () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("displays only maybe count when all attendees tentative", () => { it("displays only TENTATIVE count when all attendees tentative", () => {
const attendees = [ const attendees = [
{ {
cn: "organizer", cn: "organizer",
@@ -1262,7 +1264,7 @@ describe("Event Preview Display", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("displays correct counts with multiple yes and maybe", () => { it("displays correct counts with multiple yes and TENTATIVE", () => {
const attendees = [ const attendees = [
{ {
cn: "Guest 1", cn: "Guest 1",
+1 -1
View File
@@ -258,7 +258,7 @@ describe("EventPopover", () => {
{ {
cn: "John Doe", cn: "John Doe",
cal_address: "john@example.com", cal_address: "john@example.com",
partstat: "NEED_ACTION", partstat: "NEEDS-ACTION",
rsvp: "FALSE", rsvp: "FALSE",
role: "REQ-PARTICIPANT", role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL", cutype: "INDIVIDUAL",
@@ -315,7 +315,7 @@ describe("EventPreviewModal - Recurring Event Interactions", () => {
); );
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "eventPreview.accept" }) screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
); );
await waitFor(() => { await waitFor(() => {
@@ -505,7 +505,7 @@ describe("RSVP to Recurring Event", () => {
); );
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "eventPreview.accept" }) screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
); );
await waitFor(() => { await waitFor(() => {
@@ -543,7 +543,7 @@ describe("RSVP to Recurring Event", () => {
); );
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "eventPreview.accept" }) screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
); );
await waitFor(() => { await waitFor(() => {
@@ -975,8 +975,7 @@ describe("handleRSVP function", () => {
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"], basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
basePreloadedState.user, basePreloadedState.user,
nonRecurringEvent, nonRecurringEvent,
"ACCEPTED", "ACCEPTED"
mockOnClose
); );
expect(mockDispatch).toHaveBeenCalled(); expect(mockDispatch).toHaveBeenCalled();
+4 -4
View File
@@ -13,7 +13,7 @@ import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
import CalendarLayout from "../../../src/components/Calendar/CalendarLayout"; import CalendarLayout from "../../../src/components/Calendar/CalendarLayout";
import { renderWithProviders } from "../../utils/Renderwithproviders"; import { renderWithProviders } from "../../utils/Renderwithproviders";
import { SpiedFunction } from "jest-mock"; import { SpiedFunction } from "jest-mock";
import { Calendars } from "../../../src/features/Calendars/CalendarTypes"; import { Calendar } from "../../../src/features/Calendars/CalendarTypes";
import { CalendarEvent } from "../../../src/features/Events/EventsTypes"; import { CalendarEvent } from "../../../src/features/Events/EventsTypes";
import { DateSelectArg } from "@fullcalendar/core"; import { DateSelectArg } from "@fullcalendar/core";
@@ -27,7 +27,7 @@ describe("Update tempcalendars called with correct params", () => {
let refreshCalendarsSpy: SpiedFunction< let refreshCalendarsSpy: SpiedFunction<
( (
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendars: Calendars[], calendars: Calendar[],
calendarRange: { start: Date; end: Date }, calendarRange: { start: Date; end: Date },
calType?: "temp" calType?: "temp"
) => Promise<void> ) => Promise<void>
@@ -35,14 +35,14 @@ describe("Update tempcalendars called with correct params", () => {
let refreshSingularCalendarSpy: SpiedFunction< let refreshSingularCalendarSpy: SpiedFunction<
( (
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendar: Calendars, calendar: Calendar,
calendarRange: { start: Date; end: Date }, calendarRange: { start: Date; end: Date },
calType?: "temp" calType?: "temp"
) => Promise<void> ) => Promise<void>
>; >;
let updateTempCalendarSpy: SpiedFunction< let updateTempCalendarSpy: SpiedFunction<
( (
tempcalendars: Record<string, Calendars>, tempcalendars: Record<string, Calendar>,
event: CalendarEvent, event: CalendarEvent,
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendarRange: { start: Date; end: Date } calendarRange: { start: Date; end: Date }
+10 -10
View File
@@ -71,9 +71,9 @@ describe("parseCalendarEvent", () => {
cn: "Bob", cn: "Bob",
cal_address: "bob@example.com", cal_address: "bob@example.com",
partstat: "ACCEPTED", partstat: "ACCEPTED",
rsvp: "", cutype: "INDIVIDUAL",
role: "", role: "REQ-PARTICIPANT",
cutype: "", rsvp: "FALSE",
}, },
]); ]);
}); });
@@ -133,9 +133,9 @@ describe("parseCalendarEvent", () => {
cn: "Bob", cn: "Bob",
cal_address: "bob@example.com", cal_address: "bob@example.com",
partstat: "ACCEPTED", partstat: "ACCEPTED",
rsvp: "", cutype: "INDIVIDUAL",
role: "", role: "REQ-PARTICIPANT",
cutype: "", rsvp: "FALSE",
}, },
]); ]);
}); });
@@ -285,10 +285,10 @@ describe("parseCalendarEvent", () => {
{ {
cn: "", cn: "",
cal_address: "john@example.com", cal_address: "john@example.com",
partstat: "", partstat: "NEEDS-ACTION",
rsvp: "", cutype: "INDIVIDUAL",
role: "", role: "REQ-PARTICIPANT",
cutype: "", rsvp: "FALSE",
}, },
]); ]);
+14 -15
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { userAttendee } from "../../features/User/userDataTypes"; import { userAttendee } from "../../features/User/models/attendee";
import { createAttendee } from "../../features/User/models/attendee.mapper";
import { import {
PeopleSearch, PeopleSearch,
User, User,
@@ -20,18 +21,18 @@ export default function UserSearch({
) => React.ReactNode; ) => React.ReactNode;
}) { }) {
const [selectedUsers, setSelectedUsers] = useState( const [selectedUsers, setSelectedUsers] = useState(
attendees.map((a) => ({ attendees.map((attendee) => ({
email: a.cal_address, email: attendee.cal_address,
displayName: a.cn ?? "", displayName: attendee.cn ?? "",
avatarUrl: "", avatarUrl: "",
openpaasId: "", openpaasId: "",
})) ?? [] })) ?? []
); );
useEffect(() => { useEffect(() => {
setSelectedUsers( setSelectedUsers(
attendees.map((a) => ({ attendees.map((attendee) => ({
email: a.cal_address, email: attendee.cal_address,
displayName: a.cn ?? "", displayName: attendee.cn ?? "",
avatarUrl: "", avatarUrl: "",
openpaasId: "", openpaasId: "",
})) }))
@@ -45,14 +46,12 @@ export default function UserSearch({
inputSlot={inputSlot} inputSlot={inputSlot}
onChange={(event: any, value: User[]) => { onChange={(event: any, value: User[]) => {
setAttendees( setAttendees(
value.map((a: User) => ({ value.map((attendee: User) =>
cn: a.displayName, createAttendee({
cal_address: a.email, cal_address: attendee.email,
partstat: "NEED_ACTION", cn: attendee.displayName,
rsvp: "FALSE", })
role: "REQ-PARTICIPANT", )
cutype: "INDIVIDUAL",
}))
); );
setSelectedUsers(value); setSelectedUsers(value);
}} }}
+2 -2
View File
@@ -15,13 +15,13 @@ import {
exportCalendar, exportCalendar,
getSecretLink, getSecretLink,
} from "../../features/Calendars/CalendarApi"; } from "../../features/Calendars/CalendarApi";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import { FieldWithLabel } from "../Event/components/FieldWithLabel"; import { FieldWithLabel } from "../Event/components/FieldWithLabel";
import { SnackbarAlert } from "../Loading/SnackBarAlert"; import { SnackbarAlert } from "../Loading/SnackBarAlert";
import { useI18n } from "twake-i18n"; import { useI18n } from "twake-i18n";
import { ErrorSnackbar } from "../Error/ErrorSnackbar"; import { ErrorSnackbar } from "../Error/ErrorSnackbar";
export function AccessTab({ calendar }: { calendar: Calendars }) { export function AccessTab({ calendar }: { calendar: Calendar }) {
const { t } = useI18n(); const { t } = useI18n();
const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`; const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`;
+2 -2
View File
@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { MenuItem } from "@mui/material"; import { MenuItem } from "@mui/material";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import { CalendarName } from "./CalendarName"; import { CalendarName } from "./CalendarName";
export function CalendarItemList( export function CalendarItemList(
userPersonalCalendars: Calendars[] userPersonalCalendars: Calendar[]
): React.ReactNode { ): React.ReactNode {
return Object.values(userPersonalCalendars).map((calendar) => ( return Object.values(userPersonalCalendars).map((calendar) => (
<MenuItem key={calendar.id} value={calendar.id}> <MenuItem key={calendar.id} value={calendar.id}>
+2 -2
View File
@@ -7,7 +7,7 @@ import {
patchACLCalendarAsync, patchACLCalendarAsync,
patchCalendarAsync, patchCalendarAsync,
} from "../../features/Calendars/CalendarSlice"; } from "../../features/Calendars/CalendarSlice";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import { ResponsiveDialog } from "../Dialog"; import { ResponsiveDialog } from "../Dialog";
import { AccessTab } from "./AccessTab"; import { AccessTab } from "./AccessTab";
import { ImportTab } from "./ImportTab"; import { ImportTab } from "./ImportTab";
@@ -25,7 +25,7 @@ function CalendarPopover({
event: object | null, event: object | null,
reason: "backdropClick" | "escapeKeyDown" reason: "backdropClick" | "escapeKeyDown"
) => void; ) => void;
calendar?: Calendars; calendar?: Calendar;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
+2 -2
View File
@@ -1,7 +1,7 @@
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import SquareRoundedIcon from "@mui/icons-material/SquareRounded"; import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
export function CalendarName({ calendar }: { calendar: Calendars }) { export function CalendarName({ calendar }: { calendar: Calendar }) {
return ( return (
<Box style={{ display: "flex", flexDirection: "row", gap: 8 }}> <Box style={{ display: "flex", flexDirection: "row", gap: 8 }}>
<SquareRoundedIcon <SquareRoundedIcon
+2 -2
View File
@@ -10,7 +10,7 @@ import { useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { getCalendars } from "../../features/Calendars/CalendarApi"; import { getCalendars } from "../../features/Calendars/CalendarApi";
import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice"; import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { PeopleSearch, User } from "../Attendees/PeopleSearch"; import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { ResponsiveDialog } from "../Dialog"; import { ResponsiveDialog } from "../Dialog";
@@ -93,7 +93,7 @@ function SelectedCalendarsList({
onRemove, onRemove,
onColorChange, onColorChange,
}: { }: {
calendars: Record<string, Calendars>; calendars: Record<string, Calendar>;
selectedCal: CalendarWithOwner[]; selectedCal: CalendarWithOwner[];
onRemove: (cal: CalendarWithOwner) => void; onRemove: (cal: CalendarWithOwner) => void;
onColorChange: ( onColorChange: (
@@ -6,7 +6,7 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import { useState, useMemo, useEffect } from "react"; import { useState, useMemo, useEffect } from "react";
import CalendarPopover from "./CalendarModal"; import CalendarPopover from "./CalendarModal";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import MoreVertIcon from "@mui/icons-material/MoreVert"; import MoreVertIcon from "@mui/icons-material/MoreVert";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Checkbox from "@mui/material/Checkbox"; import Checkbox from "@mui/material/Checkbox";
@@ -206,7 +206,7 @@ function CalendarSelector({
handleCalendarToggle, handleCalendarToggle,
setOpen, setOpen,
}: { }: {
calendars: Record<string, Calendars>; calendars: Record<string, Calendar>;
id: string; id: string;
isPersonal: boolean; isPersonal: boolean;
selectedCalendars: string[]; selectedCalendars: string[];
@@ -1,5 +1,5 @@
import Dialog from "@mui/material/Dialog"; import Dialog from "@mui/material/Dialog";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
@@ -17,7 +17,7 @@ export function DeleteCalendarDialog({
}: { }: {
deletePopupOpen: boolean; deletePopupOpen: boolean;
setDeletePopupOpen: (e: boolean) => void; setDeletePopupOpen: (e: boolean) => void;
calendars: Record<string, Calendars>; calendars: Record<string, Calendar>;
id: string; id: string;
isPersonal: boolean; isPersonal: boolean;
handleDeleteConfirm: () => void; handleDeleteConfirm: () => void;
+2 -2
View File
@@ -12,7 +12,7 @@ import {
import { useI18n } from "twake-i18n"; import { useI18n } from "twake-i18n";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useAppSelector } from "../../app/hooks"; import { useAppSelector } from "../../app/hooks";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import { AddDescButton } from "../Event/AddDescButton"; import { AddDescButton } from "../Event/AddDescButton";
import { ColorPicker } from "./CalendarColorPicker"; import { ColorPicker } from "./CalendarColorPicker";
@@ -35,7 +35,7 @@ export function SettingsTab({
setColor: Function; setColor: Function;
visibility: "public" | "private"; visibility: "public" | "private";
setVisibility: Function; setVisibility: Function;
calendar?: Calendars; calendar?: Calendar;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const [toggleDesc, setToggleDesc] = useState(Boolean(description)); const [toggleDesc, setToggleDesc] = useState(Boolean(description));
@@ -6,7 +6,7 @@ import {
getTempCalendarsListAsync, getTempCalendarsListAsync,
removeTempCal, removeTempCal,
} from "../../features/Calendars/CalendarSlice"; } from "../../features/Calendars/CalendarSlice";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import { setView } from "../../features/Settings/SettingsSlice"; import { setView } from "../../features/Settings/SettingsSlice";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { User, PeopleSearch } from "../Attendees/PeopleSearch"; import { User, PeopleSearch } from "../Attendees/PeopleSearch";
@@ -134,7 +134,7 @@ function getCalendarsFromUsersDelta(
return { calendarsToImport, calendarsToToggle }; return { calendarsToImport, calendarsToToggle };
} }
function buildEmailToCalendarMap(calRecord: Record<string, Calendars>) { function buildEmailToCalendarMap(calRecord: Record<string, Calendar>) {
const map = new Map<string, string[]>(); const map = new Map<string, string[]>();
for (const [id, cal] of Object.entries(calRecord)) { for (const [id, cal] of Object.entries(calRecord)) {
cal.ownerEmails?.forEach((email) => { cal.ownerEmails?.forEach((email) => {
@@ -1,7 +1,7 @@
import { DateSelectArg } from "@fullcalendar/core"; import { DateSelectArg } from "@fullcalendar/core";
import { CalendarApi } from "@fullcalendar/core"; import { CalendarApi } from "@fullcalendar/core";
import { CalendarEvent } from "../../../features/Events/EventsTypes"; import { CalendarEvent } from "../../../features/Events/EventsTypes";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
import { getDeltaInMilliseconds } from "../../../utils/dateUtils"; import { getDeltaInMilliseconds } from "../../../utils/dateUtils";
import { import {
getCalendarDetailAsync, getCalendarDetailAsync,
@@ -17,21 +17,22 @@ import { refreshCalendars } from "../../Event/utils/eventUtils";
import { updateTempCalendar } from "../utils/calendarUtils"; import { updateTempCalendar } from "../utils/calendarUtils";
import { User } from "../../Attendees/PeopleSearch"; import { User } from "../../Attendees/PeopleSearch";
import { formatLocalDateTime } from "../../Event/utils/dateTimeFormatters"; import { formatLocalDateTime } from "../../Event/utils/dateTimeFormatters";
import { userAttendee } from "../../../features/User/userDataTypes"; import { userAttendee } from "../../../features/User/models/attendee";
import { createAttendee } from "../../../features/User/models/attendee.mapper";
export interface EventHandlersProps { export interface EventHandlersProps {
setSelectedRange: (range: DateSelectArg | null) => void; setSelectedRange: (range: DateSelectArg | null) => void;
setAnchorEl: (el: HTMLElement | null) => void; setAnchorEl: (el: HTMLElement | null) => void;
calendarRef: React.RefObject<CalendarApi | null>; calendarRef: React.RefObject<CalendarApi | null>;
selectedCalendars: string[]; selectedCalendars: string[];
tempcalendars: Record<string, Calendars>; tempcalendars: Record<string, Calendar>;
calendarRange: { start: Date; end: Date }; calendarRange: { start: Date; end: Date };
dispatch: any; dispatch: any;
setOpenEventDisplay: (open: boolean) => void; setOpenEventDisplay: (open: boolean) => void;
setEventDisplayedId: (id: string) => void; setEventDisplayedId: (id: string) => void;
setEventDisplayedCalId: (id: string) => void; setEventDisplayedCalId: (id: string) => void;
setEventDisplayedTemp: (temp: boolean) => void; setEventDisplayedTemp: (temp: boolean) => void;
calendars: Record<string, Calendars>; calendars: Record<string, Calendar>;
setSelectedEvent: (event: CalendarEvent) => void; setSelectedEvent: (event: CalendarEvent) => void;
setAfterChoiceFunc: (func: Function) => void; setAfterChoiceFunc: (func: Function) => void;
setOpenEditModePopup: (open: string) => void; setOpenEditModePopup: (open: string) => void;
@@ -72,14 +73,13 @@ export const createEventHandlers = (props: EventHandlersProps) => {
end: selectInfo?.end end: selectInfo?.end
? formatLocalDateTime(selectInfo?.end, timezone) ? formatLocalDateTime(selectInfo?.end, timezone)
: "", : "",
attendee: tempUsers.map((u) => ({ attendee: tempUsers.map((user) =>
cn: u.displayName, createAttendee({
cal_address: u.email, cal_address: user.email,
partstat: "NEED-ACTION", cn: user.displayName,
role: "REQ-PARTICIPANT", rsvp: "TRUE",
rsvp: "TRUE", })
cutype: "INDIVIDUAL", ),
})),
} as CalendarEvent; } as CalendarEvent;
setTempEvent(newEvent); setTempEvent(newEvent);
@@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import { CalendarApi, NowIndicatorContentArg } from "@fullcalendar/core"; import { CalendarApi, NowIndicatorContentArg } from "@fullcalendar/core";
import { createMouseHandlers } from "./mouseHandlers"; import { createMouseHandlers } from "./mouseHandlers";
import { userAttendee } from "../../../features/User/userDataTypes"; import { userAttendee } from "../../../features/User/models/attendee";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
import { EventErrorHandler } from "../../Error/EventErrorHandler"; import { EventErrorHandler } from "../../Error/EventErrorHandler";
import { EventChip } from "../../Event/EventChip/EventChip"; import { EventChip } from "../../Event/EventChip/EventChip";
@@ -11,8 +11,8 @@ export interface ViewHandlersProps {
setSelectedDate: (date: Date) => void; setSelectedDate: (date: Date) => void;
setSelectedMiniDate: (date: Date) => void; setSelectedMiniDate: (date: Date) => void;
onViewChange?: (view: string) => void; onViewChange?: (view: string) => void;
calendars: Record<string, Calendars>; calendars: Record<string, Calendar>;
tempcalendars: Record<string, Calendars>; tempcalendars: Record<string, Calendar>;
errorHandler: EventErrorHandler; errorHandler: EventErrorHandler;
} }
@@ -1,10 +1,10 @@
import { darken, getContrastRatio, lighten, Theme } from "@mui/material"; import { darken, getContrastRatio, lighten, Theme } from "@mui/material";
import { ThunkDispatch } from "@reduxjs/toolkit"; import { ThunkDispatch } from "@reduxjs/toolkit";
import { updateCalColor } from "../../../features/Calendars/CalendarSlice"; import { updateCalColor } from "../../../features/Calendars/CalendarSlice";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
export function updateDarkColor( export function updateDarkColor(
calendars: Record<string, Calendars>, calendars: Record<string, Calendar>,
theme: Theme, theme: Theme,
dispatch: ThunkDispatch<any, any, any> dispatch: ThunkDispatch<any, any, any>
) { ) {
@@ -1,5 +1,5 @@
import { CalendarEvent } from "../../../features/Events/EventsTypes"; import { CalendarEvent } from "../../../features/Events/EventsTypes";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils"; import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice"; import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice";
import { SlotLabelContentArg } from "@fullcalendar/core"; import { SlotLabelContentArg } from "@fullcalendar/core";
@@ -140,7 +140,7 @@ export const eventToFullCalendarFormat = (
export const extractEvents = ( export const extractEvents = (
selectedCalendars: string[], selectedCalendars: string[],
calendars: Record<string, Calendars>, calendars: Record<string, Calendar>,
userAddress?: string, userAddress?: string,
hideDeclinedEvents?: boolean | null hideDeclinedEvents?: boolean | null
) => { ) => {
@@ -272,7 +272,7 @@ export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" {
} }
export async function updateTempCalendar( export async function updateTempCalendar(
tempcalendars: Record<string, Calendars>, tempcalendars: Record<string, Calendar>,
event: CalendarEvent, event: CalendarEvent,
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendarRange: { start: Date; end: Date } calendarRange: { start: Date; end: Date }
@@ -4,8 +4,8 @@ import LockOutlineIcon from "@mui/icons-material/LockOutline";
import { Box, getContrastRatio } from "@mui/material"; import { Box, getContrastRatio } from "@mui/material";
import moment from "moment"; import moment from "moment";
import React, { useLayoutEffect, useState } from "react"; import React, { useLayoutEffect, useState } from "react";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
import { userAttendee } from "../../../features/User/userDataTypes"; import { userAttendee } from "../../../features/User/models/attendee";
import { EventErrorHandler } from "../../Error/EventErrorHandler"; import { EventErrorHandler } from "../../Error/EventErrorHandler";
import { EVENT_DURATION } from "./EventChip"; import { EVENT_DURATION } from "./EventChip";
@@ -13,8 +13,8 @@ const COMPACT_WIDTH_THRESHOLD = 100;
export interface EventChipProps { export interface EventChipProps {
arg: any; arg: any;
calendars: Record<string, Calendars>; calendars: Record<string, Calendar>;
tempcalendars: Record<string, Calendars>; tempcalendars: Record<string, Calendar>;
errorHandler: EventErrorHandler; errorHandler: EventErrorHandler;
} }
export interface IconDisplayConfig { export interface IconDisplayConfig {
+3 -3
View File
@@ -25,8 +25,8 @@ import {
import AttendeeSelector from "../Attendees/AttendeeSearch"; import AttendeeSelector from "../Attendees/AttendeeSearch";
import RepeatEvent from "./EventRepeat"; import RepeatEvent from "./EventRepeat";
import { RepetitionObject } from "../../features/Events/EventsTypes"; import { RepetitionObject } from "../../features/Events/EventsTypes";
import { userAttendee } from "../../features/User/userDataTypes"; import { userAttendee } from "../../features/User/models/attendee";
import { Calendars } from "../../features/Calendars/CalendarTypes"; import { Calendar } from "../../features/Calendars/CalendarTypes";
import { import {
generateMeetingLink, generateMeetingLink,
addVideoConferenceToDescription, addVideoConferenceToDescription,
@@ -87,7 +87,7 @@ interface EventFormFieldsProps {
isOpen?: boolean; isOpen?: boolean;
// Data // Data
userPersonalCalendars: Calendars[]; userPersonalCalendars: Calendar[];
timezoneList: { timezoneList: {
zones: string[]; zones: string[];
browserTz: string; browserTz: string;
@@ -1,51 +1,128 @@
import { ThunkDispatch } from "@reduxjs/toolkit"; import { ThunkDispatch } from "@reduxjs/toolkit";
import { useAppSelector } from "../../../app/hooks";
import { import {
updateEventInstanceAsync, updateEventInstanceAsync,
updateSeriesAsync,
putEventAsync, putEventAsync,
deleteEventInstanceAsync, deleteEventInstanceAsync,
deleteEventAsync, deleteEventAsync,
} from "../../../features/Calendars/CalendarSlice"; } from "../../../features/Calendars/CalendarSlice";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
import { import { updateSeriesPartstat } from "../../../features/Events/EventApi";
getEvent,
updateSeriesPartstat,
} from "../../../features/Events/EventApi";
import { CalendarEvent } from "../../../features/Events/EventsTypes"; import { CalendarEvent } from "../../../features/Events/EventsTypes";
import { PartStat } from "../../../features/User/models/attendee";
import { createAttendee } from "../../../features/User/models/attendee.mapper";
import { userData } from "../../../features/User/userDataTypes"; import { userData } from "../../../features/User/userDataTypes";
import { buildFamilyName } from "../../../utils/buildFamilyName";
import { getCalendarRange } from "../../../utils/dateUtils"; import { getCalendarRange } from "../../../utils/dateUtils";
import { refreshCalendars } from "../utils/eventUtils"; import { refreshCalendars } from "../utils/eventUtils";
function updateEventAttendees(
event: CalendarEvent,
user: userData | undefined,
rsvp: PartStat
) {
if (!user) {
throw new Error("Cannot update attendees without user data");
}
const eventHasNoAttendees = !event?.attendee || event.attendee.length === 0;
const isOrganizer =
!event.organizer ||
event.organizer.cal_address?.toLowerCase() === user.email?.toLowerCase();
if (eventHasNoAttendees) {
const userdata = createAttendee({
cal_address: user.email,
cn: buildFamilyName(user.given_name, user.family_name, user.email),
role: isOrganizer ? "CHAIR" : "REQ-PARTICIPANT",
partstat: rsvp,
});
return {
organizer: isOrganizer ? userdata : event.organizer,
attendee: [userdata],
};
}
return {
attendee: (() => {
const userEmailLower = user.email?.toLowerCase();
const userExists = event.attendee.some(
(attendee) => attendee.cal_address?.toLowerCase() === userEmailLower
);
const updatedAttendees = event.attendee.map((attendeeData) =>
attendeeData.cal_address?.toLowerCase() === userEmailLower
? { ...attendeeData, partstat: rsvp }
: attendeeData
);
if (!userExists) {
const newUserAttendee = createAttendee({
cal_address: user.email,
cn: buildFamilyName(user.given_name, user.family_name, user.email),
role: "REQ-PARTICIPANT",
partstat: rsvp,
});
return [...updatedAttendees, newUserAttendee];
}
return updatedAttendees;
})(),
};
}
async function handleSoloRSVP(
dispatch: ThunkDispatch<any, any, any>,
calendar: Calendar,
event: CalendarEvent
) {
dispatch(updateEventInstanceAsync({ cal: calendar, event }));
}
async function handleAllRSVP(
dispatch: ThunkDispatch<any, any, any>,
event: CalendarEvent,
userEmail: string,
rsvp: PartStat,
calendars: Calendar[]
) {
const calendarRange = getCalendarRange(new Date(event.start));
await updateSeriesPartstat(event, userEmail, rsvp);
await refreshCalendars(dispatch, calendars, calendarRange);
}
async function handleDefaultRSVP(
dispatch: ThunkDispatch<any, any, any>,
calendar: Calendar,
newEvent: CalendarEvent
) {
dispatch(putEventAsync({ cal: calendar, newEvent }));
}
export async function handleRSVP( export async function handleRSVP(
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendar: Calendars, calendar: Calendar,
user: { userData: userData }, user: userData | undefined,
event: CalendarEvent, event: CalendarEvent,
rsvp: string, rsvp: PartStat,
onClose?: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void,
typeOfAction?: string, typeOfAction?: string,
calendars?: Calendars[] calendars?: Calendar[]
) { ) {
const newEvent = { const newEvent = {
...event, ...event,
attendee: event.attendee?.map((a) => ...updateEventAttendees(event, user, rsvp),
a.cal_address === user.userData?.email ? { ...a, partstat: rsvp } : a
),
}; };
if (typeOfAction === "solo") { if (typeOfAction === "solo") {
dispatch(updateEventInstanceAsync({ cal: calendar, event: newEvent })); await handleSoloRSVP(dispatch, calendar, newEvent);
} else if (typeOfAction === "all") { } else if (typeOfAction === "all") {
const calendarRange = getCalendarRange(new Date(event.start)); if (!calendars || calendars.length === 0) {
throw new Error("Cannot update all occurrences without calendar list");
// Update PARTSTAT on ALL VEVENTs (master + exceptions)
await updateSeriesPartstat(event, user.userData?.email, rsvp);
if (calendars) {
await refreshCalendars(dispatch, calendars, calendarRange);
} }
if (!user?.email) {
throw new Error("Cannot update all occurrences without user email");
}
await handleAllRSVP(dispatch, event, user.email, rsvp, calendars);
} else { } else {
dispatch(putEventAsync({ cal: calendar, newEvent })); await handleDefaultRSVP(dispatch, calendar, newEvent);
} }
} }
@@ -54,7 +131,7 @@ export function handleDelete(
typeOfAction: "solo" | "all" | undefined, typeOfAction: "solo" | "all" | undefined,
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void, onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void,
dispatch: Function, dispatch: Function,
calendar: Calendars, calendar: Calendar,
event: CalendarEvent, event: CalendarEvent,
calId: string, calId: string,
eventId: string eventId: string
+4 -4
View File
@@ -10,8 +10,8 @@ import {
getCalendarDetailAsync, getCalendarDetailAsync,
getCalendarsListAsync, getCalendarsListAsync,
} from "../../../features/Calendars/CalendarSlice"; } from "../../../features/Calendars/CalendarSlice";
import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { Calendar } from "../../../features/Calendars/CalendarTypes";
import { userAttendee } from "../../../features/User/userDataTypes"; import { userAttendee } from "../../../features/User/models/attendee";
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils"; import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
export function renderAttendeeBadge( export function renderAttendeeBadge(
@@ -111,7 +111,7 @@ export function stringAvatar(name: string) {
export async function refreshCalendars( export async function refreshCalendars(
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendars: Calendars[], calendars: Calendar[],
calendarRange: { start: Date; end: Date }, calendarRange: { start: Date; end: Date },
calType?: "temp" calType?: "temp"
) { ) {
@@ -163,7 +163,7 @@ export async function refreshCalendars(
export async function refreshSingularCalendar( export async function refreshSingularCalendar(
dispatch: ThunkDispatch<any, any, any>, dispatch: ThunkDispatch<any, any, any>,
calendar: Calendars, calendar: Calendar,
calendarRange: { start: Date; end: Date }, calendarRange: { start: Date; end: Date },
calType?: "temp" calType?: "temp"
) { ) {
+14 -17
View File
@@ -24,10 +24,11 @@ import { useI18n } from "twake-i18n";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { searchEventsAsync } from "../../features/Search/SearchSlice"; import { searchEventsAsync } from "../../features/Search/SearchSlice";
import { setView } from "../../features/Settings/SettingsSlice"; import { setView } from "../../features/Settings/SettingsSlice";
import { userAttendee } from "../../features/User/userDataTypes"; import { userAttendee } from "../../features/User/models/attendee";
import UserSearch from "../Attendees/AttendeeSearch"; import UserSearch from "../Attendees/AttendeeSearch";
import { CalendarItemList } from "../Calendar/CalendarItemList"; import { CalendarItemList } from "../Calendar/CalendarItemList";
import { PeopleSearch, User } from "../Attendees/PeopleSearch"; import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { createAttendee } from "../../features/User/models/attendee.mapper";
export default function SearchBar() { export default function SearchBar() {
const { t } = useI18n(); const { t } = useI18n();
@@ -151,14 +152,12 @@ export default function SearchBar() {
if (contacts.length > 0) { if (contacts.length > 0) {
handleSearch("", { handleSearch("", {
...filters, ...filters,
organizers: contacts.map((c) => ({ organizers: contacts.map((contact) =>
cal_address: c.email || c.displayName || "", createAttendee({
cutype: "INDIVIDUAL", cal_address: contact.email,
cn: c.displayName || c.email, cn: contact.displayName,
role: "Participant", })
rsvp: "TRUE", ),
partstat: "",
})),
}); });
} }
}; };
@@ -334,14 +333,12 @@ export default function SearchBar() {
handleFilterChange("keywords", query); handleFilterChange("keywords", query);
handleFilterChange( handleFilterChange(
"organizers", "organizers",
selectedContacts.map((a: User) => ({ selectedContacts.map((attendee: User) =>
cn: a.displayName, createAttendee({
cal_address: a.email || "", cal_address: attendee.email,
partstat: "NEEDS-ACTION", cn: attendee.displayName,
rsvp: "FALSE", })
role: "REQ-PARTICIPANT", )
cutype: "INDIVIDUAL",
}))
); );
}} }}
> >
+22 -22
View File
@@ -1,5 +1,5 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Calendars } from "./CalendarTypes"; import { Calendar } from "./CalendarTypes";
import { CalendarEvent } from "../Events/EventsTypes"; import { CalendarEvent } from "../Events/EventsTypes";
import { import {
addSharedCalendar, addSharedCalendar,
@@ -39,7 +39,7 @@ interface RejectedError {
} }
export const getCalendarsListAsync = createAsyncThunk< export const getCalendarsListAsync = createAsyncThunk<
{ importedCalendars: Record<string, Calendars>; errors: string }, // Return type { importedCalendars: Record<string, Calendar>; errors: string }, // Return type
void, // Arg type void, // Arg type
{ rejectValue: RejectedError; state: any } // ThunkAPI config { rejectValue: RejectedError; state: any } // ThunkAPI config
>("calendars/getCalendars", async (_, { rejectWithValue, getState }) => { >("calendars/getCalendars", async (_, { rejectWithValue, getState }) => {
@@ -52,7 +52,7 @@ export const getCalendarsListAsync = createAsyncThunk<
} }
try { try {
const importedCalendars: Record<string, Calendars> = {}; const importedCalendars: Record<string, Calendar> = {};
const user = (await getOpenPaasUser()) as Record<string, string>; const user = (await getOpenPaasUser()) as Record<string, string>;
const calendars = (await getCalendars(user.id)) as Record<string, any>; const calendars = (await getCalendars(user.id)) as Record<string, any>;
const rawCalendars = calendars._embedded["dav:calendar"] as Record< const rawCalendars = calendars._embedded["dav:calendar"] as Record<
@@ -159,12 +159,12 @@ export const getCalendarsListAsync = createAsyncThunk<
}); });
export const getTempCalendarsListAsync = createAsyncThunk< export const getTempCalendarsListAsync = createAsyncThunk<
Record<string, Calendars>, Record<string, Calendar>,
User, User,
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => { >("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => {
try { try {
const importedCalendars: Record<string, Calendars> = {}; const importedCalendars: Record<string, Calendar> = {};
const calendars = (await getCalendars( const calendars = (await getCalendars(
tempUser.openpaasId ?? "", tempUser.openpaasId ?? "",
@@ -267,7 +267,7 @@ export const getCalendarDetailAsync = createAsyncThunk<
export const putEventAsync = createAsyncThunk< export const putEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[]; calType?: "temp" }, { calId: string; events: CalendarEvent[]; calType?: "temp" },
{ cal: Calendars; newEvent: CalendarEvent; calType?: "temp" }, { cal: Calendar; newEvent: CalendarEvent; calType?: "temp" },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
"calendars/putEvent", "calendars/putEvent",
@@ -395,7 +395,7 @@ export const removeCalendarAsync = createAsyncThunk<
export const moveEventAsync = createAsyncThunk< export const moveEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, { calId: string; events: CalendarEvent[] },
{ cal: Calendars; newEvent: CalendarEvent; newURL: string }, { cal: Calendar; newEvent: CalendarEvent; newURL: string },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
"calendars/moveEvent", "calendars/moveEvent",
@@ -490,7 +490,7 @@ export const deleteEventAsync = createAsyncThunk<
export const deleteEventInstanceAsync = createAsyncThunk< export const deleteEventInstanceAsync = createAsyncThunk<
{ calId: string; eventId: string }, { calId: string; eventId: string },
{ cal: Calendars; event: CalendarEvent }, { cal: Calendar; event: CalendarEvent },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => { >("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => {
try { try {
@@ -506,7 +506,7 @@ export const deleteEventInstanceAsync = createAsyncThunk<
export const updateEventInstanceAsync = createAsyncThunk< export const updateEventInstanceAsync = createAsyncThunk<
{ calId: string; event: CalendarEvent }, { calId: string; event: CalendarEvent },
{ cal: Calendars; event: CalendarEvent }, { cal: Calendar; event: CalendarEvent },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
"calendars/updateEventInstance", "calendars/updateEventInstance",
@@ -525,7 +525,7 @@ export const updateEventInstanceAsync = createAsyncThunk<
export const updateSeriesAsync = createAsyncThunk< export const updateSeriesAsync = createAsyncThunk<
void, void,
{ cal: Calendars; event: CalendarEvent; removeOverrides?: boolean }, { cal: Calendar; event: CalendarEvent; removeOverrides?: boolean },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
"calendars/updateSeries", "calendars/updateSeries",
@@ -659,13 +659,13 @@ export const importEventFromFileAsync = createAsyncThunk<
const CalendarSlice = createSlice({ const CalendarSlice = createSlice({
name: "calendars", name: "calendars",
initialState: { initialState: {
list: {} as Record<string, Calendars>, list: {} as Record<string, Calendar>,
templist: {} as Record<string, Calendars>, templist: {} as Record<string, Calendar>,
pending: false, pending: false,
error: null as string | null, error: null as string | null,
} as { } as {
list: Record<string, Calendars>; list: Record<string, Calendar>;
templist: Record<string, Calendars>; templist: Record<string, Calendar>;
pending: boolean; pending: boolean;
error: string | null; error: string | null;
}, },
@@ -675,7 +675,7 @@ const CalendarSlice = createSlice({
action: PayloadAction<Record<string, string | Record<string, string>>> 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 Calendar;
state.list[id].name = action.payload.name as string; state.list[id].name = action.payload.name as string;
state.list[id].color = action.payload.color as Record<string, string>; state.list[id].color = action.payload.color as Record<string, string>;
state.list[id].description = action.payload.description as string; state.list[id].description = action.payload.description as string;
@@ -753,7 +753,7 @@ const CalendarSlice = createSlice({
( (
state, state,
action: PayloadAction<{ action: PayloadAction<{
importedCalendars: Record<string, Calendars>; importedCalendars: Record<string, Calendar>;
errors: string; errors: string;
}> }>
) => { ) => {
@@ -766,7 +766,7 @@ const CalendarSlice = createSlice({
) )
.addCase( .addCase(
getTempCalendarsListAsync.fulfilled, getTempCalendarsListAsync.fulfilled,
(state, action: PayloadAction<Record<string, Calendars>>) => { (state, action: PayloadAction<Record<string, Calendar>>) => {
state.pending = false; state.pending = false;
Object.keys(action.payload).forEach( Object.keys(action.payload).forEach(
(id) => (state.templist[id] = action.payload[id]) (id) => (state.templist[id] = action.payload[id])
@@ -823,7 +823,7 @@ const CalendarSlice = createSlice({
state[type][action.payload.calId] = { state[type][action.payload.calId] = {
id: action.payload.calId, id: action.payload.calId,
events: {}, events: {},
} as Calendars; } as Calendar;
} }
action.payload.events.forEach((event) => { action.payload.events.forEach((event) => {
state[type][action.payload.calId].events[event.uid] = event; state[type][action.payload.calId].events[event.uid] = event;
@@ -853,7 +853,7 @@ const CalendarSlice = createSlice({
state.list[action.payload.calId] = { state.list[action.payload.calId] = {
id: action.payload.calId, id: action.payload.calId,
events: {}, events: {},
} as Calendars; } as Calendar;
} }
state.list[action.payload.calId].events[action.payload.event.uid] = state.list[action.payload.calId].events[action.payload.event.uid] =
@@ -871,7 +871,7 @@ const CalendarSlice = createSlice({
state.list[action.payload.calId] = { state.list[action.payload.calId] = {
id: action.payload.calId, id: action.payload.calId,
events: {}, events: {},
} as Calendars; } as Calendar;
} }
action.payload.events.forEach((event) => { action.payload.events.forEach((event) => {
state.list[action.payload.calId].events[event.uid] = event; state.list[action.payload.calId].events[event.uid] = event;
@@ -932,7 +932,7 @@ const CalendarSlice = createSlice({
owner: action.payload.owner, owner: action.payload.owner,
ownerEmails: action.payload.ownerEmails, ownerEmails: action.payload.ownerEmails,
events: {}, events: {},
} as Calendars; } as Calendar;
state.error = null; state.error = null;
}) })
.addCase(patchCalendarAsync.fulfilled, (state, action) => { .addCase(patchCalendarAsync.fulfilled, (state, action) => {
@@ -973,7 +973,7 @@ const CalendarSlice = createSlice({
events: {}, events: {},
owner: action.payload.owner, owner: action.payload.owner,
ownerEmails: action.payload.ownerEmails, ownerEmails: action.payload.ownerEmails,
} as Calendars; } as Calendar;
state.error = null; state.error = null;
}) })
.addCase(removeCalendarAsync.fulfilled, (state, action) => { .addCase(removeCalendarAsync.fulfilled, (state, action) => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { CalendarEvent } from "../Events/EventsTypes"; import { CalendarEvent } from "../Events/EventsTypes";
export interface Calendars { export interface Calendar {
id: string; id: string;
link: string; link: string;
name: string; name: string;
@@ -0,0 +1,56 @@
import { Box, Typography } from "@mui/material";
import { Dispatch, SetStateAction } from "react";
import { useI18n } from "twake-i18n";
import { Calendar } from "../../Calendars/CalendarTypes";
import { userData } from "../../User/userDataTypes";
import { ContextualizedEvent } from "../EventsTypes";
import { RSVPButton } from "./RSVPButton";
interface AttendanceValidationProps {
contextualizedEvent: ContextualizedEvent;
calendarList: Calendar[];
user: userData | undefined;
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
}
export function AttendanceValidation({
contextualizedEvent,
calendarList,
user,
setAfterChoiceFunc,
setOpenEditModePopup,
}: AttendanceValidationProps) {
const { currentUserAttendee, isOwn } = contextualizedEvent;
const { t } = useI18n();
// Check if we should show RSVP buttons
const hasNoAttendeesOrOrganizer =
!(contextualizedEvent.event?.attendee?.length > 0) &&
!contextualizedEvent.event?.organizer;
if (!((currentUserAttendee || hasNoAttendeesOrOrganizer) && isOwn)) {
return null;
}
const commonButtonProps = {
contextualizedEvent,
user,
calendarList,
setAfterChoiceFunc,
setOpenEditModePopup,
};
return (
<>
<Typography sx={{ marginRight: 2 }}>
{t("eventPreview.attendingQuestion")}
</Typography>
<Box display="flex" gap="15px" alignItems="center">
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
</Box>
</>
);
}
@@ -0,0 +1,67 @@
import { Button } from "@mui/material";
import { Dispatch, SetStateAction } from "react";
import { useI18n } from "twake-i18n";
import { useAppDispatch } from "../../../app/hooks";
import { Calendar } from "../../Calendars/CalendarTypes";
import { PartStat } from "../../User/models/attendee";
import { userData } from "../../User/userDataTypes";
import { ContextualizedEvent } from "../EventsTypes";
import { handleRSVPClick } from "./handleRSVPClick";
const rsvpColor: Record<PartStat, "success" | "error" | "warning" | "primary"> =
{
ACCEPTED: "success",
DECLINED: "error",
TENTATIVE: "warning",
"NEEDS-ACTION": "primary",
} as const;
interface RSVPButtonProps {
rsvpValue: PartStat;
contextualizedEvent: ContextualizedEvent;
user: userData | undefined;
calendarList: Calendar[];
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
}
export function RSVPButton({
rsvpValue,
contextualizedEvent,
user,
calendarList,
setAfterChoiceFunc,
setOpenEditModePopup,
}: RSVPButtonProps) {
const { t } = useI18n();
const dispatch = useAppDispatch();
const { currentUserAttendee } = contextualizedEvent;
return (
<Button
variant={
currentUserAttendee?.partstat === rsvpValue ? "contained" : "outlined"
}
color={
currentUserAttendee?.partstat === rsvpValue
? rsvpColor[rsvpValue]
: "primary"
}
size="large"
sx={{ borderRadius: "50px" }}
onClick={() =>
handleRSVPClick(
rsvpValue,
contextualizedEvent,
user,
calendarList,
setAfterChoiceFunc,
setOpenEditModePopup,
dispatch
)
}
>
{t(`eventPreview.${rsvpValue}`)}
</Button>
);
}
@@ -0,0 +1,43 @@
import { Dispatch, SetStateAction } from "react";
import { AppDispatch } from "../../../app/store";
import { handleRSVP } from "../../../components/Event/eventHandlers/eventHandlers";
import { Calendar } from "../../Calendars/CalendarTypes";
import { PartStat } from "../../User/models/attendee";
import { userData } from "../../User/userDataTypes";
import { ContextualizedEvent } from "../EventsTypes";
export async function handleRSVPClick(
rsvp: PartStat,
contextualizedEvent: ContextualizedEvent,
user: userData | undefined,
calendarList: Calendar[],
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
dispatch: AppDispatch
) {
const { isRecurring, calendar, event } = contextualizedEvent;
if (isRecurring) {
setAfterChoiceFunc(() => async (type: string) => {
try {
await handleRSVP(
dispatch,
calendar,
user,
event,
rsvp,
type,
calendarList
);
} catch (error) {
console.error("Error handling RSVP:", error);
}
});
setOpenEditModePopup("attendance");
} else {
try {
await handleRSVP(dispatch, calendar, user, event, rsvp);
} catch (error) {
console.error("Error handling RSVP:", error);
}
}
}
+20 -151
View File
@@ -36,10 +36,7 @@ import {
import ResponsiveDialog from "../../components/Dialog/ResponsiveDialog"; import ResponsiveDialog from "../../components/Dialog/ResponsiveDialog";
import { EditModeDialog } from "../../components/Event/EditModeDialog"; import { EditModeDialog } from "../../components/Event/EditModeDialog";
import EventDuplication from "../../components/Event/EventDuplicate"; import EventDuplication from "../../components/Event/EventDuplicate";
import { import { handleDelete } from "../../components/Event/eventHandlers/eventHandlers";
handleDelete,
handleRSVP,
} from "../../components/Event/eventHandlers/eventHandlers";
import { InfoRow } from "../../components/Event/InfoRow"; import { InfoRow } from "../../components/Event/InfoRow";
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils"; import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils"; import { getCalendarRange } from "../../utils/dateUtils";
@@ -48,8 +45,12 @@ import { dlEvent } from "./EventApi";
import { CalendarEvent } from "./EventsTypes"; import { CalendarEvent } from "./EventsTypes";
import EventUpdateModal from "./EventUpdateModal"; import EventUpdateModal from "./EventUpdateModal";
import { useI18n } from "twake-i18n"; import { useI18n } from "twake-i18n";
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/models/attendee";
import { browserDefaultTimeZone } from "../../utils/timezone"; import { browserDefaultTimeZone } from "../../utils/timezone";
import { AttendanceValidation } from "./AttendanceValidation/AttendanceValidation";
import { Calendar } from "../Calendars/CalendarTypes";
import { userData } from "../User/userDataTypes";
import { createEventContext } from "./createEventContext";
export default function EventPreviewModal({ export default function EventPreviewModal({
eventId, eventId,
@@ -77,12 +78,13 @@ export default function EventPreviewModal({
? calendars.templist[calId] ? calendars.templist[calId]
: calendars.list[calId]; : calendars.list[calId];
const event = calendar.events[eventId]; const event = calendar.events[eventId];
const user = useAppSelector((state) => state.user); const user = useAppSelector((state) => state.user.userData);
if (!user) return null;
const isRecurring = event?.uid?.includes("/"); const isRecurring = event?.uid?.includes("/");
const isOwn = calendar.ownerEmails?.includes(user.userData?.email); const isOwn = calendar.ownerEmails?.includes(user.email);
const isOrganizer = event.organizer const isOrganizer = event.organizer
? user.userData?.email === event.organizer.cal_address ? user.email === event.organizer.cal_address
: isOwn; : isOwn;
const [showAllAttendees, setShowAllAttendees] = useState(false); const [showAllAttendees, setShowAllAttendees] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false);
@@ -274,8 +276,9 @@ export default function EventPreviewModal({
: attendees.slice(0, attendeeDisplayLimit); : attendees.slice(0, attendeeDisplayLimit);
const currentUserAttendee = event.attendee?.find( const currentUserAttendee = event.attendee?.find(
(person) => person.cal_address === user.userData?.email (person) => person.cal_address === user.email
); );
const contextualizedEvent = createEventContext(event, calendar, user);
const organizer = event.attendee?.find( const organizer = event.attendee?.find(
(a) => a.cal_address === event.organizer?.cal_address (a) => a.cal_address === event.organizer?.cal_address
@@ -365,7 +368,7 @@ export default function EventPreviewModal({
window.open( window.open(
`${mailSpaUrl}/mailto/?uri=mailto:${event.attendee `${mailSpaUrl}/mailto/?uri=mailto:${event.attendee
.map((a) => a.cal_address) .map((a) => a.cal_address)
.filter((mail) => mail !== user.userData?.email) .filter((mail) => mail !== user.email)
.join(",")}?subject=${event.title}` .join(",")}?subject=${event.title}`
) )
} }
@@ -467,147 +470,13 @@ export default function EventPreviewModal({
</> </>
} }
actions={ actions={
currentUserAttendee && <AttendanceValidation
isOwn && ( contextualizedEvent={contextualizedEvent}
<> calendarList={calendarList}
<> user={user}
<Typography sx={{ marginRight: 2 }}> setAfterChoiceFunc={setAfterChoiceFunc}
{t("eventPreview.attendingQuestion")} setOpenEditModePopup={setOpenEditModePopup}
</Typography> />
<Box display="flex" gap="15px" alignItems="center">
<Button
variant={
currentUserAttendee?.partstat === "ACCEPTED"
? "contained"
: "outlined"
}
color={
currentUserAttendee?.partstat === "ACCEPTED"
? "success"
: "primary"
}
size="large"
sx={{ borderRadius: "50px" }}
onClick={() => {
if (isRecurring) {
setAfterChoiceFunc(
() => (type: string) =>
handleRSVP(
dispatch,
calendar,
user,
event,
"ACCEPTED",
onClose,
type,
calendarList
)
);
setOpenEditModePopup("attendance");
} else {
handleRSVP(
dispatch,
calendar,
user,
event,
"ACCEPTED",
onClose
);
}
}}
>
{t("eventPreview.accept")}
</Button>
<Button
variant={
currentUserAttendee?.partstat === "TENTATIVE"
? "contained"
: "outlined"
}
color={
currentUserAttendee?.partstat === "TENTATIVE"
? "warning"
: "primary"
}
size="large"
sx={{ borderRadius: "50px" }}
onClick={() => {
if (isRecurring) {
setAfterChoiceFunc(
() => (type: string) =>
handleRSVP(
dispatch,
calendar,
user,
event,
"TENTATIVE",
onClose,
type,
calendarList
)
);
setOpenEditModePopup("attendance");
} else {
handleRSVP(
dispatch,
calendar,
user,
event,
"TENTATIVE",
onClose
);
}
}}
>
{t("eventPreview.maybe")}
</Button>
<Button
variant={
currentUserAttendee?.partstat === "DECLINED"
? "contained"
: "outlined"
}
color={
currentUserAttendee?.partstat === "DECLINED"
? "error"
: "primary"
}
size="large"
sx={{ borderRadius: "50px" }}
onClick={() => {
if (isRecurring) {
setAfterChoiceFunc(
() => (type: string) =>
handleRSVP(
dispatch,
calendar,
user,
event,
"DECLINED",
onClose,
type,
calendarList
)
);
setOpenEditModePopup("attendance");
} else {
handleRSVP(
dispatch,
calendar,
user,
event,
"DECLINED",
onClose
);
}
}}
>
{t("eventPreview.decline")}
</Button>
</Box>
</>
</>
)
} }
> >
{((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( {((event.class !== "PRIVATE" && !isOwn) || isOwn) && (
+6 -6
View File
@@ -12,8 +12,8 @@ import React, {
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { ResponsiveDialog } from "../../components/Dialog"; import { ResponsiveDialog } from "../../components/Dialog";
import { putEventAsync } from "../Calendars/CalendarSlice"; import { putEventAsync } from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes"; import { Calendar } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/models/attendee";
import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { createSelector } from "@reduxjs/toolkit"; import { createSelector } from "@reduxjs/toolkit";
import { TIMEZONES } from "../../utils/timezone-data"; import { TIMEZONES } from "../../utils/timezone-data";
@@ -75,11 +75,11 @@ function EventPopover({
if (id.split("/")[0] === userId) { if (id.split("/")[0] === userId) {
return calendars.list?.[id]; return calendars.list?.[id];
} }
return {} as Calendars; return {} as Calendar;
}) })
.filter((calendar) => calendar.id) .filter((calendar) => calendar.id)
); );
const userPersonalCalendars: Calendars[] = useAppSelector( const userPersonalCalendars: Calendar[] = useAppSelector(
selectPersonalCalendars selectPersonalCalendars
); );
@@ -722,10 +722,10 @@ function EventPopover({
const newEventUID = crypto.randomUUID(); const newEventUID = crypto.randomUUID();
// Resolve target calendar safely // Resolve target calendar safely
const targetCalendar: Calendars | undefined = const targetCalendar: Calendar | undefined =
calList[calendarid] || calList[calendarid] ||
userPersonalCalendars[0] || userPersonalCalendars[0] ||
(Object.values(calList)[0] as Calendars | undefined); (Object.values(calList)[0] as Calendar | undefined);
if (!targetCalendar || !targetCalendar.id) { if (!targetCalendar || !targetCalendar.id) {
console.error("No target calendar available to save event"); console.error("No target calendar available to save event");
return; return;
+5 -5
View File
@@ -12,8 +12,8 @@ import {
updateEventLocal, updateEventLocal,
clearFetchCache, clearFetchCache,
} from "../Calendars/CalendarSlice"; } from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes"; import { Calendar } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/models/attendee";
import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { TIMEZONES } from "../../utils/timezone-data"; import { TIMEZONES } from "../../utils/timezone-data";
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils"; import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
@@ -95,10 +95,10 @@ function EventUpdateModal({
const calendarsList = useAppSelector((state) => state.calendars.list); const calendarsList = useAppSelector((state) => state.calendars.list);
const userPersonalCalendars: Calendars[] = useMemo(() => { const userPersonalCalendars: Calendar[] = useMemo(() => {
const allCalendars = Object.values(calendarsList) as Calendars[]; const allCalendars = Object.values(calendarsList) as Calendar[];
return allCalendars.filter( return allCalendars.filter(
(c: Calendars) => c.id?.split("/")[0] === user.userData?.openpaasId (c: Calendar) => c.id?.split("/")[0] === user.userData?.openpaasId
); );
}, [calendarsList, user.userData?.openpaasId]); }, [calendarsList, user.userData?.openpaasId]);
+12 -1
View File
@@ -1,4 +1,6 @@
import { userAttendee, userOrganiser } from "../User/userDataTypes"; import { Calendar } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/models/attendee";
import { userOrganiser } from "../User/userDataTypes";
export interface CalendarEvent { export interface CalendarEvent {
URL: string; URL: string;
@@ -39,3 +41,12 @@ export interface AlarmObject {
trigger: string; trigger: string;
action: string; action: string;
} }
export interface ContextualizedEvent {
event: CalendarEvent;
calendar: Calendar;
currentUserAttendee: userAttendee | undefined;
isOwn: boolean;
isRecurring: boolean;
isOrganizer: boolean;
}
+27
View File
@@ -0,0 +1,27 @@
import { Calendar } from "../Calendars/CalendarTypes";
import { userData } from "../User/userDataTypes";
import { CalendarEvent, ContextualizedEvent } from "./EventsTypes";
export function createEventContext(
event: CalendarEvent,
calendar: Calendar,
user: userData
): ContextualizedEvent {
const isOwn = calendar.ownerEmails?.includes(user.email) ?? false;
const isRecurring = event?.uid?.includes("/") ?? false;
const isOrganizer = event.organizer
? user?.email === event.organizer.cal_address
: isOwn;
const currentUserAttendee = event.attendee?.find(
(person) => person.cal_address === user.email
);
return {
event,
calendar,
currentUserAttendee,
isOwn,
isRecurring,
isOrganizer,
};
}
+12 -9
View File
@@ -1,4 +1,4 @@
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/models/attendee";
import { AlarmObject, CalendarEvent, RepetitionObject } 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";
@@ -7,6 +7,7 @@ import {
convertFormDateTimeToISO, convertFormDateTimeToISO,
detectDateTimeFormat, detectDateTimeFormat,
} from "../../components/Event/utils/dateTimeHelpers"; } from "../../components/Event/utils/dateTimeHelpers";
import { createAttendee } from "../User/models/attendee.mapper";
type RawEntry = [string, Record<string, string>, string, any]; type RawEntry = [string, Record<string, string>, string, any];
function resolveTimezoneId(tzid?: string): string | undefined { function resolveTimezoneId(tzid?: string): string | undefined {
@@ -119,14 +120,16 @@ export function parseCalendarEvent(
}; };
break; break;
case "attendee": case "attendee":
(event.attendee as userAttendee[]).push({ (event.attendee as userAttendee[]).push(
cn: params?.cn ?? "", createAttendee({
cal_address: value.replace(/^mailto:/i, ""), cn: params?.cn,
partstat: params?.partstat ?? "", cal_address: value.replace(/^mailto:/i, ""),
rsvp: params?.rsvp ?? "", partstat: params?.partstat as userAttendee["partstat"],
role: params?.role ?? "", rsvp: params?.rsvp as userAttendee["rsvp"],
cutype: params?.cutype ?? "", role: params?.role as userAttendee["role"],
}); cutype: params?.cutype as userAttendee["cutype"],
})
);
break; break;
case "dtstamp": case "dtstamp":
event.stamp = value; event.stamp = value;
+1 -1
View File
@@ -1,7 +1,7 @@
import { TIMEZONES } from "../../utils/timezone-data"; import { TIMEZONES } from "../../utils/timezone-data";
import { resolveTimezone } from "../../components/Calendar/TimezoneSelector"; import { resolveTimezone } from "../../components/Calendar/TimezoneSelector";
import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/models/attendee";
import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters"; import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters";
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils"; import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
import { browserDefaultTimeZone } from "../../utils/timezone"; import { browserDefaultTimeZone } from "../../utils/timezone";
@@ -0,0 +1,19 @@
import { userAttendee } from "./attendee";
export function createAttendee(options?: {
cal_address?: string;
cn?: string;
role?: userAttendee["role"];
partstat?: userAttendee["partstat"];
rsvp?: userAttendee["rsvp"];
cutype?: userAttendee["cutype"];
}): userAttendee {
return {
cal_address: options?.cal_address ?? "",
cn: options?.cn ?? "",
cutype: options?.cutype ?? "INDIVIDUAL",
role: options?.role ?? "REQ-PARTICIPANT",
partstat: options?.partstat ?? "NEEDS-ACTION",
rsvp: options?.rsvp ?? "FALSE",
};
}
+12
View File
@@ -0,0 +1,12 @@
export type AttendeeRole = "CHAIR" | "REQ-PARTICIPANT" | "OPT-PARTICIPANT";
export type CuType = "INDIVIDUAL" | "GROUP";
export type PartStat = "ACCEPTED" | "DECLINED" | "TENTATIVE" | "NEEDS-ACTION";
export interface userAttendee {
cal_address: string;
partstat: PartStat;
role: AttendeeRole;
cutype: CuType;
rsvp: "TRUE" | "FALSE";
cn: string;
}
-9
View File
@@ -33,12 +33,3 @@ export interface userOrganiser {
cn: string; cn: string;
cal_address: string; cal_address: string;
} }
export interface userAttendee {
cn?: string;
cal_address: string;
partstat: string;
rsvp: string;
role: string;
cutype: string;
}
+3 -3
View File
@@ -245,9 +245,9 @@
"tooltip": "Others see you as available during the time range of this event." "tooltip": "Others see you as available during the time range of this event."
}, },
"attendingQuestion": "Attending?", "attendingQuestion": "Attending?",
"accept": "Accept", "ACCEPTED": "Accept",
"maybe": "Maybe", "TENTATIVE": "Maybe",
"decline": "Decline", "DECLINED": "Decline",
"showMore": "Show more", "showMore": "Show more",
"showLess": "Show less", "showLess": "Show less",
"joinVideo": "Join the video conference", "joinVideo": "Join the video conference",
+3 -3
View File
@@ -247,9 +247,9 @@
"tooltip": "Les autres vous voient comme disponible pendant la plage horaire de cet événement." "tooltip": "Les autres vous voient comme disponible pendant la plage horaire de cet événement."
}, },
"attendingQuestion": "Vous participez ?", "attendingQuestion": "Vous participez ?",
"accept": "Accepter", "ACCEPTED": "Accepter",
"maybe": "Peut-être", "TENTATIVE": "Peut-être",
"decline": "Décliner", "DECLINED": "Décliner",
"showMore": "Afficher plus", "showMore": "Afficher plus",
"showLess": "Afficher moins", "showLess": "Afficher moins",
"joinVideo": "Rejoindre la visioconférence", "joinVideo": "Rejoindre la visioconférence",
+3 -3
View File
@@ -247,9 +247,9 @@
"tooltip": "Другие видят вас свободным." "tooltip": "Другие видят вас свободным."
}, },
"attendingQuestion": "Присоединитесь?", "attendingQuestion": "Присоединитесь?",
"accept": "Да", "ACCEPTED": "Да",
"maybe": "Возможно", "TENTATIVE": "Возможно",
"decline": "Нет", "DECLINED": "Нет",
"showMore": "Показать больше", "showMore": "Показать больше",
"showLess": "Показать меньше", "showLess": "Показать меньше",
"joinVideo": "Присоединиться к видеоконференции", "joinVideo": "Присоединиться к видеоконференции",
+3 -3
View File
@@ -245,9 +245,9 @@
"tooltip": "Người khác sẽ thấy bạn rảnh trong khoảng thời gian này." "tooltip": "Người khác sẽ thấy bạn rảnh trong khoảng thời gian này."
}, },
"attendingQuestion": "Tham gia?", "attendingQuestion": "Tham gia?",
"accept": "Chấp nhận", "ACCEPTED": "Chấp nhận",
"maybe": "Có thể", "TENTATIVE": "Có thể",
"decline": "Từ chối", "DECLINED": "Từ chối",
"showMore": "Xem thêm", "showMore": "Xem thêm",
"showLess": "Thu gọn", "showLess": "Thu gọn",
"joinVideo": "Tham gia cuộc họp video", "joinVideo": "Tham gia cuộc họp video",
+12
View File
@@ -0,0 +1,12 @@
export function buildFamilyName(
firstName: string | undefined,
lastName: string | undefined,
email: string
): string {
const trimmedFirstName = firstName?.trim() || "";
const trimmedLastName = lastName?.trim() || "";
const fullName = [trimmedFirstName, trimmedLastName]
.filter(Boolean)
.join(" ");
return fullName || email;
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { RepetitionObject } from "../features/Events/EventsTypes"; import { RepetitionObject } from "../features/Events/EventsTypes";
import { userAttendee } from "../features/User/userDataTypes"; import { userAttendee } from "../features/User/models/attendee";
export interface EventFormTempData { export interface EventFormTempData {
// Form fields // Form fields