* [#527] added panel to grant right for calendar delegation * [#527] added rights management to access modal * [#527] conditionnal rendering of access granting based on rights * [#527] saves on close except cancel * [#527] added tests
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useAppSelector } from "@/app/hooks";
|
||||
import {
|
||||
exportCalendar,
|
||||
getSecretLink,
|
||||
@@ -19,9 +20,28 @@ import { useI18n } from "twake-i18n";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { FieldWithLabel } from "../Event/components/FieldWithLabel";
|
||||
import { SnackbarAlert } from "../Loading/SnackBarAlert";
|
||||
import { CalendarAccessRights, UserWithAccess } from "./CalendarAccessRights";
|
||||
|
||||
export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
interface AccessTabProps {
|
||||
calendar: Calendar;
|
||||
usersWithAccess: UserWithAccess[];
|
||||
onUsersWithAccessChange: (users: UserWithAccess[]) => void;
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void;
|
||||
}
|
||||
|
||||
export function AccessTab({
|
||||
calendar,
|
||||
usersWithAccess,
|
||||
onUsersWithAccessChange,
|
||||
onInvitesLoaded,
|
||||
}: AccessTabProps) {
|
||||
const { t } = useI18n();
|
||||
const userData = useAppSelector((state) => state.user.userData);
|
||||
const isPersonalCalendar = userData.openpaasId === calendar.id.split("/")[0];
|
||||
const isDelegatedWithAdministration = calendar.invite?.find(
|
||||
(invite) => invite.href.includes(userData.email) && invite.access === 5
|
||||
);
|
||||
|
||||
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace(".json", "")}`;
|
||||
|
||||
const [secretLink, setSecretLink] = useState("");
|
||||
@@ -60,11 +80,8 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
const exportedData = await exportCalendar(
|
||||
calendar.link.replace(".json", "")
|
||||
);
|
||||
const blob = new Blob([exportedData], {
|
||||
type: "text/calendar",
|
||||
});
|
||||
const blob = new Blob([exportedData], { type: "text/calendar" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${calendar.id.split("/")[1]}.ics`;
|
||||
@@ -74,7 +91,6 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
setExportError((e as Error).message);
|
||||
setExportLoading(false);
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
@@ -82,6 +98,15 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isPersonalCalendar || isDelegatedWithAdministration) && (
|
||||
<CalendarAccessRights
|
||||
calendar={calendar}
|
||||
value={usersWithAccess}
|
||||
onChange={onUsersWithAccessChange}
|
||||
onInvitesLoaded={onInvitesLoaded}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!window.DAV_BASE_URL && (
|
||||
<FieldWithLabel label={t("calendar.caldav_access")} isExpanded={false}>
|
||||
<Box mt={2}>
|
||||
@@ -108,6 +133,7 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
|
||||
<FieldWithLabel label={t("calendar.secretUrl")} isExpanded={false}>
|
||||
<Box mt={3} display="flex" alignItems="center" gap={1}>
|
||||
<TextField
|
||||
@@ -147,11 +173,10 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
<FieldWithLabel label={t("calendar.exportCalendar")} isExpanded={false}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", m: 1, lineHeight: 1.5 }}
|
||||
sx={{ color: "text.secondary", my: 1, lineHeight: 1.5 }}
|
||||
>
|
||||
{t("calendar.exportDesc")}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { getUserDetails } from "@/features/User/userAPI";
|
||||
import {
|
||||
AutocompleteRenderInputParams,
|
||||
Avatar,
|
||||
Box,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@linagora/twake-mui";
|
||||
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
|
||||
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
||||
import { FieldWithLabel } from "../Event/components/FieldWithLabel";
|
||||
import { stringAvatar } from "../Event/utils/eventUtils";
|
||||
|
||||
export interface UserWithAccess extends User {
|
||||
accessRight: AccessRight;
|
||||
}
|
||||
|
||||
interface CalendarAccessRightsProps {
|
||||
calendar: Calendar;
|
||||
value: UserWithAccess[];
|
||||
onChange: (users: UserWithAccess[]) => void;
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void;
|
||||
}
|
||||
|
||||
export function CalendarAccessRights({
|
||||
calendar,
|
||||
value: usersWithAccess,
|
||||
onChange,
|
||||
onInvitesLoaded,
|
||||
}: CalendarAccessRightsProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [searchWidth, setSearchWidth] = useState<number | undefined>(undefined);
|
||||
const [accessRight, setAccessRight] = useState<AccessRight>(2);
|
||||
const [inviteLoading, setInvitesLoading] = useState(false);
|
||||
|
||||
const currentUsersRef = useRef<UserWithAccess[]>(usersWithAccess);
|
||||
useEffect(() => {
|
||||
currentUsersRef.current = usersWithAccess;
|
||||
}, [usersWithAccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setSearchWidth(entry.contentRect.width);
|
||||
}
|
||||
});
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!calendar.invite?.length) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadInvitedUsers() {
|
||||
setInvitesLoading(true);
|
||||
try {
|
||||
const loaded: UserWithAccess[] = (
|
||||
await Promise.all(
|
||||
calendar.invite.map(async (invite) => {
|
||||
const principalId = invite.principal.split("/").pop();
|
||||
if (!principalId) return null;
|
||||
try {
|
||||
const details = await getUserDetails(principalId);
|
||||
const email =
|
||||
details?.preferredEmail ?? details?.emails?.[0] ?? "";
|
||||
return {
|
||||
openpaasId: principalId,
|
||||
displayName:
|
||||
[details?.firstname, details?.lastname]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.trim() || email,
|
||||
email,
|
||||
accessRight: invite.access as AccessRight,
|
||||
} satisfies UserWithAccess;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
)
|
||||
).filter((u) => u !== null && !!u.email);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const loadedIds = new Set(loaded.map((u) => normalizeEmail(u.email)));
|
||||
const manuallyAdded = currentUsersRef.current.filter(
|
||||
(u) => !loadedIds.has(normalizeEmail(u.email))
|
||||
);
|
||||
const merged = [...loaded, ...manuallyAdded];
|
||||
|
||||
onInvitesLoaded(loaded);
|
||||
onChange(merged);
|
||||
} finally {
|
||||
if (!cancelled) setInvitesLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadInvitedUsers();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [calendar.invite, onChange, onInvitesLoaded]);
|
||||
|
||||
const normalizeEmail = (email?: string) => email?.trim().toLowerCase() ?? "";
|
||||
|
||||
const handleUserSelect = (_event: unknown, users: User[]) => {
|
||||
const updated: UserWithAccess[] = users.map((user) => {
|
||||
const existing = usersWithAccess.find(
|
||||
(u) => normalizeEmail(u.email) === normalizeEmail(user.email)
|
||||
);
|
||||
return existing ?? { ...user, accessRight };
|
||||
});
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const handleRemoveUser = (email: string) => {
|
||||
onChange(
|
||||
usersWithAccess.filter(
|
||||
(u) => normalizeEmail(u.email) !== normalizeEmail(email)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeUserRight = (email: string, right: AccessRight) => {
|
||||
onChange(
|
||||
usersWithAccess.map((u) =>
|
||||
normalizeEmail(u.email) === normalizeEmail(email)
|
||||
? { ...u, accessRight: right }
|
||||
: u
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const accessRightOptions: { value: AccessRight; label: string }[] = [
|
||||
{ value: 2, label: t("calendarPopover.access.viewAllEvents") },
|
||||
{ value: 3, label: t("calendarPopover.access.editor") },
|
||||
{ value: 5, label: t("calendarPopover.access.administrator") },
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldWithLabel
|
||||
label={t("calendarPopover.access.grantAccessRights")}
|
||||
isExpanded={false}
|
||||
>
|
||||
<Box ref={containerRef}>
|
||||
<PeopleSearch
|
||||
selectedUsers={usersWithAccess}
|
||||
onChange={handleUserSelect}
|
||||
objectTypes={["user"]}
|
||||
onToggleEventPreview={() => {}}
|
||||
customSlotProps={{
|
||||
popper: {
|
||||
anchorEl: containerRef.current,
|
||||
placement: "bottom-start",
|
||||
sx: {
|
||||
minWidth: searchWidth,
|
||||
"& .MuiPaper-root": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
customRenderInput={(
|
||||
params: AutocompleteRenderInputParams,
|
||||
query: string,
|
||||
setQuery: (value: string) => void
|
||||
) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
autoFocus
|
||||
placeholder={t("peopleSearch.label")}
|
||||
value={query}
|
||||
inputRef={(el) => {
|
||||
const ref = params.InputProps.ref;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (ref && "current" in ref) {
|
||||
(
|
||||
ref as React.MutableRefObject<HTMLInputElement | null>
|
||||
).current = el;
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
variant="outlined"
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
sx: {
|
||||
fontSize: "14px",
|
||||
"&::placeholder": { fontSize: "14px" },
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<PeopleOutlineOutlinedIcon
|
||||
sx={{ color: "text.secondary" }}
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Select
|
||||
value={accessRight}
|
||||
onChange={(e) =>
|
||||
setAccessRight(e.target.value as AccessRight)
|
||||
}
|
||||
variant="standard"
|
||||
disableUnderline
|
||||
sx={{
|
||||
fontSize: "0.875rem",
|
||||
color: "text.secondary",
|
||||
"& .MuiSelect-select": {
|
||||
paddingRight: "24px !important",
|
||||
paddingY: 0,
|
||||
},
|
||||
"& .MuiSelect-icon": { fontSize: "1rem" },
|
||||
"&:before, &:after": { display: "none" },
|
||||
}}
|
||||
>
|
||||
{accessRightOptions.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
sx={{ color: "text.secondary" }}
|
||||
>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{inviteLoading ? (
|
||||
<Box mt={2} display="flex" justifyContent="center">
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
) : (
|
||||
usersWithAccess.length > 0 && (
|
||||
<Box mt={2} display="flex" flexDirection="column" gap={1}>
|
||||
{usersWithAccess.map((user) => (
|
||||
<Box
|
||||
key={user.email}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={1}
|
||||
py={0.5}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
"&:hover": { backgroundColor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1.5} minWidth={0}>
|
||||
<Avatar
|
||||
{...stringAvatar(user.displayName)}
|
||||
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
||||
>
|
||||
{user.displayName?.[0]?.toUpperCase()}
|
||||
</Avatar>
|
||||
<Box
|
||||
minWidth={0}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={0}
|
||||
>
|
||||
<Typography noWrap>{user.displayName}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{user.email}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={0.5}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Select
|
||||
value={user.accessRight}
|
||||
onChange={(e) =>
|
||||
handleChangeUserRight(
|
||||
user.email,
|
||||
e.target.value as AccessRight
|
||||
)
|
||||
}
|
||||
variant="standard"
|
||||
disableUnderline
|
||||
sx={{
|
||||
fontSize: "0.875rem",
|
||||
color: "text.secondary",
|
||||
"& .MuiSelect-select": {
|
||||
paddingRight: "24px !important",
|
||||
paddingY: 0,
|
||||
},
|
||||
"& .MuiSelect-icon": { fontSize: "1rem" },
|
||||
}}
|
||||
>
|
||||
{accessRightOptions.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
sx={{ color: "text.secondary" }}
|
||||
>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={t("actions.remove")}
|
||||
onClick={() => handleRemoveUser(user.email)}
|
||||
sx={{ color: "text.secondary" }}
|
||||
>
|
||||
<HighlightOffIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</FieldWithLabel>
|
||||
);
|
||||
}
|
||||
@@ -6,15 +6,19 @@ import {
|
||||
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 { useEffect, useState } from "react";
|
||||
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";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
|
||||
function CalendarPopover({
|
||||
open,
|
||||
@@ -42,6 +46,13 @@ function CalendarPopover({
|
||||
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);
|
||||
@@ -70,27 +81,82 @@ function CalendarPopover({
|
||||
setVisibility("public");
|
||||
setImportTarget("new");
|
||||
}
|
||||
setUsersWithAccess([]);
|
||||
initialUsersRef.current = [];
|
||||
}, [calendar, open]);
|
||||
|
||||
const updateCalendar = (calId: string, calLink: string) => {
|
||||
dispatch(
|
||||
patchCalendarAsync({
|
||||
calId,
|
||||
calLink,
|
||||
patch: { name: name.trim(), desc: description.trim(), color },
|
||||
})
|
||||
);
|
||||
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) {
|
||||
dispatch(
|
||||
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) return;
|
||||
|
||||
await dispatch(
|
||||
updateDelegationCalendarAsync({
|
||||
calId: calendar?.id,
|
||||
calLink,
|
||||
share: { set, remove },
|
||||
})
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
const createCalendar = async (
|
||||
calId: string,
|
||||
name: string,
|
||||
@@ -116,15 +182,19 @@ function CalendarPopover({
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
if (calendar) {
|
||||
updateCalendar(calendar.id, calendar.link);
|
||||
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);
|
||||
}
|
||||
handleClose({}, "backdropClick");
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
@@ -162,9 +232,14 @@ function CalendarPopover({
|
||||
|
||||
const handleClose = (
|
||||
e: object | null,
|
||||
reason: "backdropClick" | "escapeKeyDown"
|
||||
reason: "backdropClick" | "escapeKeyDown" | "cancel"
|
||||
): void => {
|
||||
onClose(e, reason);
|
||||
if (reason !== "cancel") {
|
||||
handleSave();
|
||||
onClose(e, reason);
|
||||
} else {
|
||||
onClose(e, "backdropClick");
|
||||
}
|
||||
setName("");
|
||||
setDescription("");
|
||||
setColor(defaultColors[0]);
|
||||
@@ -172,7 +247,9 @@ function CalendarPopover({
|
||||
setVisibility("public");
|
||||
setImportTarget("new");
|
||||
setImportedContent(null);
|
||||
|
||||
setUsersWithAccess([]);
|
||||
initialUsersRef.current = [];
|
||||
setSaveError("");
|
||||
setNewCalName("");
|
||||
setNewCalDescription("");
|
||||
setNewCalColor(defaultColors[0]);
|
||||
@@ -203,16 +280,17 @@ function CalendarPopover({
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => handleClose({}, "backdropClick")}
|
||||
>
|
||||
<Button variant="outlined" onClick={() => handleClose({}, "cancel")}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={tab === "import" ? !importedContent : !name.trim()}
|
||||
variant="contained"
|
||||
onClick={tab === "import" ? handleImport : handleSave}
|
||||
onClick={
|
||||
tab === "import"
|
||||
? handleImport
|
||||
: () => handleClose({}, "backdropClick")
|
||||
}
|
||||
>
|
||||
{tab === "import"
|
||||
? t("actions.import")
|
||||
@@ -254,7 +332,15 @@ function CalendarPopover({
|
||||
calendar={calendar}
|
||||
/>
|
||||
)}
|
||||
{tab === "access" && calendar && <AccessTab calendar={calendar} />}
|
||||
{tab === "access" && calendar && (
|
||||
<AccessTab
|
||||
calendar={calendar}
|
||||
usersWithAccess={usersWithAccess}
|
||||
onUsersWithAccessChange={handleUsersWithAccessChange}
|
||||
onInvitesLoaded={handleInvitesLoaded}
|
||||
/>
|
||||
)}
|
||||
<ErrorSnackbar error={saveError} type="calendar" />
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user