edf158f13f
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
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<Record<string, string>>(defaultColors[0]);
|
|
const [visibility, setVisibility] = useState<"private" | "public">("public");
|
|
|
|
// access tab state
|
|
const [usersWithAccess, setUsersWithAccess] = useState<UserWithAccess[]>([]);
|
|
|
|
// 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<UserWithAccess[]>([]);
|
|
|
|
// import tab state
|
|
const [tab, setTab] = useState<"settings" | "access" | "import">("settings");
|
|
const [importedContent, setImportedContent] = useState<File | null>(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<string, string>,
|
|
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 (
|
|
<ResponsiveDialog
|
|
open={open}
|
|
onClose={() => handleClose({}, "backdropClick")}
|
|
title={
|
|
<Tabs value={tab} onChange={(_e, v) => setTab(v)}>
|
|
<Tab
|
|
value="settings"
|
|
label={
|
|
calendar
|
|
? t("calendarPopover.tabs.settings")
|
|
: t("calendarPopover.tabs.addNew")
|
|
}
|
|
/>
|
|
{calendar && (
|
|
<Tab value="access" label={t("calendarPopover.tabs.access")} />
|
|
)}
|
|
{isOwn && (
|
|
<Tab value="import" label={t("calendarPopover.tabs.import")} />
|
|
)}
|
|
</Tabs>
|
|
}
|
|
actions={
|
|
<>
|
|
<Button variant="outlined" onClick={() => handleClose({}, "cancel")}>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button
|
|
disabled={tab === "import" ? !importedContent : !name.trim()}
|
|
variant="contained"
|
|
onClick={
|
|
tab === "import"
|
|
? handleImport
|
|
: () => handleClose({}, "backdropClick")
|
|
}
|
|
>
|
|
{tab === "import"
|
|
? t("actions.import")
|
|
: calendar
|
|
? t("actions.save")
|
|
: t("actions.create")}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
{tab === "import" && (
|
|
<ImportTab
|
|
importTarget={importTarget}
|
|
setImportTarget={setImportTarget}
|
|
setImportedContent={setImportedContent}
|
|
userId={userData.openpaasId ?? ""}
|
|
newCalParams={{
|
|
name: newCalName,
|
|
setName: setNewCalName,
|
|
description: newCalDescription,
|
|
setDescription: setNewCalDescription,
|
|
color: newCalColor,
|
|
setColor: setNewCalColor,
|
|
visibility: newCalVisibility,
|
|
setVisibility: setNewCalVisibility,
|
|
}}
|
|
/>
|
|
)}
|
|
{tab === "settings" && (
|
|
<SettingsTab
|
|
name={name}
|
|
setName={setName}
|
|
description={description}
|
|
setDescription={setDescription}
|
|
color={color}
|
|
setColor={setColor}
|
|
visibility={visibility}
|
|
setVisibility={setVisibility}
|
|
calendar={calendar}
|
|
/>
|
|
)}
|
|
{tab === "access" && calendar && (
|
|
<AccessTab
|
|
calendar={calendar}
|
|
usersWithAccess={usersWithAccess}
|
|
onUsersWithAccessChange={handleUsersWithAccessChange}
|
|
onInvitesLoaded={handleInvitesLoaded}
|
|
/>
|
|
)}
|
|
<ErrorSnackbar error={saveError} type="calendar" />
|
|
</ResponsiveDialog>
|
|
);
|
|
}
|
|
|
|
export default CalendarPopover;
|