import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { Calendar } from "@/features/Calendars/CalendarTypes"; import { createCalendarAsync, importEventFromFileAsync, patchACLCalendarAsync, patchCalendarAsync, } from "@/features/Calendars/services"; import { updateDelegationCalendarAsync } from "@/features/Calendars/services/updateDelegationCalendarAsync"; import { accessRightToDavProp } from "@/utils/accessRightToDavProp"; import { defaultColors } from "@/utils/defaultColors"; import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; import { Button, Tab, Tabs } from "@linagora/twake-mui"; import { useCallback, useEffect, useRef, useState } from "react"; import { useI18n } from "twake-i18n"; import { ResponsiveDialog } from "../Dialog"; import { ErrorSnackbar } from "../Error/ErrorSnackbar"; import { AccessTab } from "./AccessTab"; import { UserWithAccess } from "./CalendarAccessRights"; import { ImportTab } from "./ImportTab"; import { SettingsTab } from "./SettingsTab"; function CalendarPopover({ open, onClose, calendar, }: { open: boolean; onClose: ( event: object | null, reason: "backdropClick" | "escapeKeyDown" ) => void; calendar?: Calendar; }) { const { t } = useI18n(); const dispatch = useAppDispatch(); const userData = useAppSelector((state) => state.user.userData) ?? {}; const calendars = useAppSelector((state) => state.calendars.list); const isOwn = calendar?.id ? extractEventBaseUuid(calendar.id) === userData.openpaasId : true; const canManageInvites = isOwn || !!calendar?.invite?.some((invite) => { const inviteEmail = invite.href .replace(/^mailto:/i, "") .trim() .toLowerCase(); const currentEmail = userData.email?.trim().toLowerCase(); return inviteEmail === currentEmail && invite.access === 5; }); // existing calendar params const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [color, setColor] = useState>(defaultColors[0]); const [visibility, setVisibility] = useState<"private" | "public">("public"); // access tab state const [usersWithAccess, setUsersWithAccess] = useState([]); // Snapshot of the invitee list as loaded from calendar.invite on open. // Used to diff on save: what changed vs what was removed. const initialUsersRef = useRef([]); // import tab state const [tab, setTab] = useState<"settings" | "access" | "import">("settings"); const [importedContent, setImportedContent] = useState(null); const [importTarget, setImportTarget] = useState("new"); // new calendar params (for import new) const [newCalName, setNewCalName] = useState(""); const [newCalDescription, setNewCalDescription] = useState(""); const [newCalColor, setNewCalColor] = useState(defaultColors[0]); const [newCalVisibility, setNewCalVisibility] = useState< "public" | "private" >("public"); useEffect(() => { if (!open) return; if (calendar) { setName(calendar.name); setDescription(calendar.description ?? ""); setColor(calendar.color ?? defaultColors[0]); setVisibility(calendar.visibility ?? "public"); setImportTarget(calendar.id ?? "new"); } else { setName(""); setDescription(""); setColor(defaultColors[0]); setVisibility("public"); setImportTarget("new"); } setUsersWithAccess([]); initialUsersRef.current = []; }, [calendar, open]); const handleUsersWithAccessChange = useCallback((users: UserWithAccess[]) => { setUsersWithAccess(users); }, []); const handleInvitesLoaded = useCallback((users: UserWithAccess[]) => { if (initialUsersRef.current.length === 0) { initialUsersRef.current = users; } }, []); const [saveError, setSaveError] = useState(""); const updateCalendar = async (calId: string, calLink: string) => { const nameChanged = name.trim() !== calendar?.name; const descChanged = description.trim() !== (calendar?.description ?? ""); const colorChanged = JSON.stringify(color) !== JSON.stringify(calendar?.color ?? defaultColors[0]); if (nameChanged || descChanged || colorChanged) { await dispatch( patchCalendarAsync({ calId, calLink, patch: { name: name.trim(), desc: description.trim(), color }, }) ).unwrap(); } if (visibility !== calendar?.visibility) { await dispatch( patchACLCalendarAsync({ calId, calLink, request: visibility === "public" ? "{DAV:}read" : "", }) ).unwrap(); } }; async function updateCalendarInvites(calLink: string) { const normaliseEmail = (u: UserWithAccess) => u.email?.trim().toLowerCase() ?? ""; const currentMap = new Map( usersWithAccess .filter((u) => !!normaliseEmail(u)) .map((u) => [normaliseEmail(u), u]) ); const set = usersWithAccess .filter((u) => !!normaliseEmail(u)) .map((u) => ({ "dav:href": `mailto:${normaliseEmail(u)}`, [accessRightToDavProp(u.accessRight)]: true, })); const remove = initialUsersRef.current .filter((u) => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u))) .map((u) => ({ "dav:href": `mailto:${normaliseEmail(u)}` })); if ((set.length === 0 && remove.length === 0) || !canManageInvites) return; await dispatch( updateDelegationCalendarAsync({ calId: calendar?.id, calLink, share: { set, remove }, }) ).unwrap(); } const createCalendar = async ( calId: string, name: string, desc: string, color: Record, visibility: string ) => { await dispatch( createCalendarAsync({ name: name.trim(), desc: desc.trim(), color: color, userData, calId, }) ); dispatch( patchACLCalendarAsync({ calId: `${userData.openpaasId}/${calId}`, calLink: `/calendars/${userData.openpaasId}/${calId}.json`, request: visibility === "public" ? "{DAV:}read" : "", }) ); }; const handleSave = async () => { if (!name.trim()) return; if (calendar) { try { await updateCalendar(calendar.id, calendar.link); await updateCalendarInvites(calendar.link); } catch { setSaveError(t("error.title")); return; } } else { createCalendar(crypto.randomUUID(), name, description, color, visibility); } }; const handleImport = async () => { if (importTarget === "new") { const calId = crypto.randomUUID(); if (newCalName.trim()) { await createCalendar( calId, newCalName, newCalDescription, newCalColor, newCalVisibility ); if (importedContent) { dispatch( importEventFromFileAsync({ calLink: `/calendar/${userData.openpaasId}/${calId}.json`, file: importedContent, }) ); } } } else { if (importedContent) { dispatch( importEventFromFileAsync({ calLink: calendars[importTarget].link, file: importedContent, }) ); } } handleClose({}, "backdropClick"); }; const handleClose = ( e: object | null, reason: "backdropClick" | "escapeKeyDown" | "cancel" ): void => { if (reason !== "cancel") { handleSave(); onClose(e, reason); } else { onClose(e, "backdropClick"); } setName(""); setDescription(""); setColor(defaultColors[0]); setTab("settings"); setVisibility("public"); setImportTarget("new"); setImportedContent(null); setUsersWithAccess([]); initialUsersRef.current = []; setSaveError(""); setNewCalName(""); setNewCalDescription(""); setNewCalColor(defaultColors[0]); setNewCalVisibility("public"); }; return ( handleClose({}, "backdropClick")} title={ setTab(v)}> {calendar && ( )} {isOwn && ( )} } actions={ <> } > {tab === "import" && ( )} {tab === "settings" && ( )} {tab === "access" && calendar && ( )} ); } export default CalendarPopover;