#599 ability to register to a resource calendar (#631)

Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
lethemanh
2026-03-17 15:33:15 +07:00
committed by GitHub
parent a9e7b1ac82
commit 0dd7f1b2fd
20 changed files with 1647 additions and 75 deletions
+20 -58
View File
@@ -1,9 +1,8 @@
import { getAccessiblePair } from "@/components/Calendar/utils/calendarColorsUtils";
import { stringAvatar } from "@/components/Event/utils/eventUtils";
import { useUserSearch } from "./useUserSearch";
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
import CloseIcon from "@mui/icons-material/Close";
import { searchUsers } from "@/features/User/userAPI";
import {
Autocomplete,
Avatar,
@@ -22,8 +21,6 @@ import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined
import {
HTMLAttributes,
useCallback,
useEffect,
useState,
type ReactNode,
type SyntheticEvent,
} from "react";
@@ -82,21 +79,29 @@ export function PeopleSearch({
getChipIcon?: (user: User) => ReactNode;
}) {
const { t } = useI18n();
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<User[]>([]);
const [hasSearched, setHasSearched] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const searchPlaceholder = placeholder ?? t("peopleSearch.placeholder");
const errorMessage = t("peopleSearch.searchError");
const {
query,
setQuery,
loading,
options,
hasSearched,
isOpen,
setIsOpen,
inputError,
setInputError,
snackbarOpen,
setSnackbarOpen,
snackbarMessage,
setSnackbarMessage,
} = useUserSearch<User>({ objectTypes, errorMessage });
const isValidEmail = (email: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const [inputError, setInputError] = useState<string | null>(null);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
const theme = useTheme();
const searchPlaceholder = placeholder ?? t("peopleSearch.placeholder");
const handleBlurCommit = useCallback(
(event: React.SyntheticEvent) => {
const trimmed = query.trim();
@@ -116,52 +121,9 @@ export function PeopleSearch({
onChange(event, [...selectedUsers, newUser]);
setQuery("");
},
[query, selectedUsers, onChange, t]
[query, selectedUsers, onChange, t, setInputError, setQuery]
);
useEffect(() => {
let cancelled = false;
const delayDebounceFn = setTimeout(async () => {
if (!query.trim()) {
if (!cancelled) {
setOptions([]);
setLoading(false);
setHasSearched(false);
}
return;
}
if (!cancelled) {
setLoading(true);
setHasSearched(false);
}
try {
const res = await searchUsers(query, objectTypes);
if (!cancelled) {
setOptions(res);
setHasSearched(true);
}
} catch {
if (!cancelled) {
setHasSearched(false);
setSnackbarMessage(t("peopleSearch.searchError"));
setSnackbarOpen(true);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}, 300);
return () => {
cancelled = true;
clearTimeout(delayDebounceFn);
};
}, [objectTypes, query, t]);
const defaultRenderInput = useCallback(
(params: AutocompleteRenderInputParams) => {
const inputProps = {
+54
View File
@@ -0,0 +1,54 @@
import { Avatar } from "@linagora/twake-mui";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import CalendarMonthOutlinedIcon from "@mui/icons-material/CalendarMonthOutlined";
import PhoneIphoneOutlinedIcon from "@mui/icons-material/PhoneIphoneOutlined";
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
import VideoCameraBackOutlinedIcon from "@mui/icons-material/VideoCameraBackOutlined";
import MeetingRoomOutlinedIcon from "@mui/icons-material/MeetingRoomOutlined";
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
import ViewComfyAltOutlinedIcon from "@mui/icons-material/ViewComfyAltOutlined";
import TvOutlinedIcon from "@mui/icons-material/TvOutlined";
import type { SvgIconComponent } from "@mui/icons-material";
/**
* displayName → MUI icon mapping.
* Add new entries here when new resources are created.
*/
const RESOURCE_ICON_MAP: Record<string, SvgIconComponent> = {
"Astreinte OSSA": PeopleAltOutlinedIcon,
"Chômage Partiel": CalendarMonthOutlinedIcon,
Congés: CalendarMonthOutlinedIcon,
COPIL: CalendarMonthOutlinedIcon,
"Iphone 11 Pro Max": PhoneIphoneOutlinedIcon,
"Note de Service": DescriptionOutlinedIcon,
"OSS Events": CalendarMonthOutlinedIcon,
"Permanence OSSA": PeopleAltOutlinedIcon,
"Projo salle 404": VideoCameraBackOutlinedIcon,
"Salle-bat-A-S215-visio-15p": MeetingRoomOutlinedIcon,
"Salle-bat-A-S216-ecran-5p": MeetingRoomOutlinedIcon,
"Salle-bat-B-S217-10p": MeetingRoomOutlinedIcon,
"Salle-bat-C-S218-ecran-20p": MeetingRoomOutlinedIcon,
"Salle-bat-D-S218-visio-amphi-200p": MeetingRoomOutlinedIcon,
Télétravail: LayersOutlinedIcon,
"TV - VN": TvOutlinedIcon,
"Veille COPIL": LayersOutlinedIcon,
"White Board - VN": ViewComfyAltOutlinedIcon,
};
function getIconForDisplayName(displayName: string): SvgIconComponent {
return RESOURCE_ICON_MAP[displayName] ?? LayersOutlinedIcon;
}
interface ResourceIconProps {
displayName: string;
}
export function ResourceIcon({ displayName }: ResourceIconProps) {
const IconComponent = getIconForDisplayName(displayName);
return (
<Avatar sx={{ backgroundColor: "transparent" }}>
<IconComponent fontSize="medium" />
</Avatar>
);
}
+335
View File
@@ -0,0 +1,335 @@
import { getAccessiblePair } from "@/components/Calendar/utils/calendarColorsUtils";
import { useUserSearch } from "./useUserSearch";
import { ResourceIcon } from "./ResourceIcon";
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
import {
Autocomplete,
Chip,
CircularProgress,
ListItem,
ListItemAvatar,
ListItemText,
PaperProps,
PopperProps,
TextField,
useTheme,
Typography,
type AutocompleteRenderInputParams,
} from "@linagora/twake-mui";
import SearchIcon from "@mui/icons-material/Search";
import {
HTMLAttributes,
useCallback,
type ReactNode,
type SyntheticEvent,
} from "react";
import { useI18n } from "twake-i18n";
export interface Resource {
email?: string;
displayName: string;
avatarUrl?: string;
openpaasId?: string;
color?: Record<string, string>;
}
export interface ExtendedAutocompleteRenderInputParams extends AutocompleteRenderInputParams {
error?: boolean;
helperText?: string | null;
placeholder?: string;
label?: string;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
export function ResourceSearch({
selectedResources,
onChange,
objectTypes,
disabled,
freeSolo,
onToggleEventPreview,
placeholder,
inputSlot,
customRenderInput,
customSlotProps,
hideLabel,
}: {
selectedResources: Resource[];
onChange: (event: SyntheticEvent, users: Resource[]) => void;
objectTypes: string[];
disabled?: boolean;
freeSolo?: boolean;
onToggleEventPreview?: () => void;
placeholder?: string;
inputSlot?: (
params: ExtendedAutocompleteRenderInputParams
) => React.ReactNode;
customRenderInput?: (
params: AutocompleteRenderInputParams,
query: string,
setQuery: (value: string) => void
) => ReactNode;
customSlotProps?: {
popper?: Partial<PopperProps>;
paper?: Partial<PaperProps>;
listbox?: Partial<HTMLAttributes<HTMLUListElement>>;
};
hideLabel?: boolean;
}) {
const { t } = useI18n();
const searchPlaceholder = placeholder ?? t("resourceSearch.placeholder");
const errorMessage = t("resourceSearch.searchError");
const {
query,
setQuery,
loading,
options,
hasSearched,
isOpen,
setIsOpen,
inputError,
setInputError,
snackbarOpen,
setSnackbarOpen,
snackbarMessage,
setSnackbarMessage,
} = useUserSearch<Resource>({ objectTypes, errorMessage });
const theme = useTheme();
const handleBlurCommit = useCallback(
(event: React.SyntheticEvent) => {
const trimmed = query.trim();
if (!trimmed) return;
if (selectedResources.find((u) => u.displayName === trimmed)) {
setQuery("");
return;
}
setInputError(null);
const newResource: Resource = { displayName: trimmed };
onChange(event, [...selectedResources, newResource]);
setQuery("");
},
[query, selectedResources, onChange, setInputError, setQuery]
);
const defaultRenderInput = useCallback(
(params: AutocompleteRenderInputParams) => {
const inputProps = {
...params.InputProps,
startAdornment: (
<>
{!selectedResources?.length ? (
<SearchIcon
fontSize="small"
sx={{ mr: 1, color: "action.active" }}
/>
) : null}
{params.InputProps.startAdornment}
</>
),
endAdornment: (
<>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{!selectedResources?.length ? params.InputProps.endAdornment : null}
</>
),
};
const enhancedParams = {
...params,
InputProps: inputProps,
inputProps: {
...params.inputProps,
autoComplete: "off",
},
};
const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && onToggleEventPreview) {
e.preventDefault();
onToggleEventPreview();
}
};
const defaultTextFieldProps = {
error: !!inputError,
helperText: inputError,
placeholder: searchPlaceholder,
label: "",
onKeyDown: handleEnterKey,
slotProps: {
input: {
...inputProps,
},
},
};
if (inputSlot) {
return (
<>
{!hideLabel && (
<Typography variant="h6" sx={{ marginBottom: "10px" }}>
{t("resourceSearch.label")}
</Typography>
)}
{inputSlot({
...enhancedParams,
error: !!inputError,
helperText: inputError,
placeholder: searchPlaceholder,
label: "",
onKeyDown: handleEnterKey,
})}
</>
);
}
return (
<>
{!hideLabel && (
<Typography variant="h6" sx={{ marginBottom: "10px" }}>
{t("resourceSearch.label")}
</Typography>
)}
<TextField
{...enhancedParams}
{...defaultTextFieldProps}
InputProps={inputProps}
size="medium"
/>
</>
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
inputError,
t,
onToggleEventPreview,
loading,
searchPlaceholder,
selectedResources?.length,
]
);
return (
<>
<Autocomplete
popupIcon={null}
freeSolo={freeSolo}
multiple
options={options}
autoComplete={false}
clearOnBlur={false}
onBlur={freeSolo ? handleBlurCommit : undefined}
open={
customRenderInput
? isOpen && !!query && (loading || options.length > 0)
: isOpen && !!query && (loading || hasSearched)
}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
disabled={disabled}
loading={loading}
filterOptions={(options: Resource[]) => options}
fullWidth
noOptionsText={t("resourceSearch.noResults")}
loadingText={t("resourceSearch.loading")}
getOptionLabel={(option: Resource | string) => {
if (typeof option === "object") {
return option.displayName;
} else {
return option;
}
}}
sx={{
"& .MuiAutocomplete-inputRoot": {
py: 0,
},
}}
filterSelectedOptions
value={selectedResources}
inputValue={query}
onInputChange={(_event, value: string) => setQuery(value)}
onChange={(event, value: string[] | Resource[]) => {
setInputError(null);
const mapped = value
.map((v: string | Resource) =>
typeof v === "string" ? { displayName: v.trim() } : v
)
.filter((v) => v.displayName.trim().length > 0);
onChange(event, mapped);
}}
slotProps={{
...customSlotProps,
popper: {
placement: "bottom-start",
sx: { minWidth: "300px", ...customSlotProps?.popper?.sx },
...customSlotProps?.popper,
},
}}
// When render input is custom, the adornments should be handled by the custom component
forcePopupIcon={!customRenderInput}
disableClearable={!!customRenderInput}
renderInput={(params) =>
customRenderInput
? customRenderInput(params, query, setQuery)
: defaultRenderInput(params)
}
renderOption={(props, option: Resource) => {
if (
selectedResources.find((u) => u.displayName === option.displayName)
)
return null;
const { key, ...otherProps } = props;
return (
<ListItem
key={key + option?.displayName}
{...otherProps}
disableGutters
>
<ListItemAvatar>
<ResourceIcon displayName={option.displayName} />
</ListItemAvatar>
<ListItemText primary={option.displayName} />
</ListItem>
);
}}
renderValue={(value: string[] | Resource[], getTagProps) =>
value.map((option: string | Resource, index) => {
const isString = typeof option === "string";
const label = isString ? option : option.displayName;
const chipColor = isString
? theme.palette.grey[300]
: (option.color?.light ?? theme.palette.grey[300]);
const textColor = getAccessiblePair(chipColor, theme);
return (
<Chip
{...getTagProps({ index })}
key={label}
style={{
backgroundColor: chipColor,
color: textColor,
}}
label={label}
/>
);
})
}
/>
<SnackbarAlert
open={snackbarOpen}
setOpen={(open: boolean) => {
setSnackbarOpen(open);
if (!open) {
setSnackbarMessage("");
}
}}
message={snackbarMessage}
severity="error"
/>
</>
);
}
+81
View File
@@ -0,0 +1,81 @@
import { useState, useEffect } from "react";
import { searchUsers } from "@/features/User/userAPI";
export interface UseUserSearchProps {
objectTypes: string[];
errorMessage: string;
}
export function useUserSearch<T>({
objectTypes,
errorMessage,
}: UseUserSearchProps) {
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<T[]>([]);
const [hasSearched, setHasSearched] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [inputError, setInputError] = useState<string | null>(null);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
useEffect(() => {
let cancelled = false;
const delayDebounceFn = setTimeout(async () => {
if (!query.trim()) {
if (!cancelled) {
setOptions([]);
setLoading(false);
setHasSearched(false);
}
return;
}
if (!cancelled) {
setLoading(true);
setHasSearched(false);
}
try {
const res = await searchUsers(query, objectTypes);
if (!cancelled) {
setOptions(res as unknown as T[]);
setHasSearched(true);
}
} catch {
if (!cancelled) {
setHasSearched(false);
setSnackbarMessage(errorMessage);
setSnackbarOpen(true);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}, 300);
return () => {
cancelled = true;
clearTimeout(delayDebounceFn);
};
}, [objectTypes, query, errorMessage]);
return {
query,
setQuery,
loading,
options,
hasSearched,
isOpen,
setIsOpen,
inputError,
setInputError,
snackbarOpen,
setSnackbarOpen,
snackbarMessage,
setSnackbarMessage,
};
}
@@ -0,0 +1,344 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { getCalendars } from "@/features/Calendars/CalendarApi";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { CalendarData } from "@/features/Calendars/types/CalendarData";
import { renameDefault } from "@/utils/renameDefault";
import {
Box,
Button,
IconButton,
TextField,
Typography,
useTheme,
} from "@linagora/twake-mui";
import CloseIcon from "@mui/icons-material/Close";
import { useRef, useState } from "react";
import { useI18n } from "twake-i18n";
import { ResponsiveDialog } from "../Dialog";
import { ColorPicker } from "./CalendarColorPicker";
import { getAccessiblePair } from "./utils/calendarColorsUtils";
import { defaultColors } from "@/utils/defaultColors";
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
import { Resource, ResourceSearch } from "../Attendees/ResourceSearch";
import { ResourceIcon } from "../Attendees/ResourceIcon";
interface CalendarWithOwner {
cal: CalendarData;
owner: Resource;
}
function CalendarItem({
cal,
onRemove,
onColorChange,
}: {
cal: CalendarWithOwner;
onRemove: () => void;
onColorChange: (color: Record<string, string>) => void;
}) {
const theme = useTheme();
const { t } = useI18n();
return (
<Box
key={cal.cal["dav:name"]}
display="flex"
justifyContent="space-between"
gap={2}
>
<Box display="flex" alignItems="center" gap={1}>
<ResourceIcon displayName={cal.owner.displayName} />
<Typography variant="body1">
{renameDefault(cal.cal["dav:name"], cal.owner.displayName, t, false)}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<ColorPicker
selectedColor={{
light: cal.cal["apple:color"] ?? defaultColors[0].light,
dark: cal.cal["apple:color"]
? getAccessiblePair(cal.cal["apple:color"], theme)
: defaultColors[0].dark,
}}
onChange={onColorChange}
/>
<IconButton size="small" onClick={onRemove}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Box>
);
}
function SelectedCalendarsList({
calendars,
selectedCal,
onRemove,
onColorChange,
}: {
calendars: Record<string, Calendar>;
selectedCal: CalendarWithOwner[];
onRemove: (cal: CalendarWithOwner) => void;
onColorChange: (
cal: CalendarWithOwner,
color: Record<string, string>
) => void;
}) {
const { t } = useI18n();
if (selectedCal.length === 0) return null;
const groupedByOwner = selectedCal.reduce<
Record<
string,
{
owner: Resource;
visibleCals: CalendarWithOwner[];
alreadyExisting: boolean;
}
>
>((acc, cal) => {
const exists = Object.values(calendars).some(
(existing: Calendar) =>
existing.id ===
cal.cal?._links?.self?.href
?.replace("/calendars/", "")
.replace(".json", "")
);
if (!acc[cal.owner.displayName]) {
acc[cal.owner.displayName] = {
owner: cal.owner,
visibleCals: [],
alreadyExisting: false,
};
}
if (exists) {
acc[cal.owner.displayName].alreadyExisting = true;
} else {
acc[cal.owner.displayName].visibleCals.push(cal);
}
return acc;
}, {});
return (
<Box mt={2}>
<Typography variant="h6" sx={{ margin: 0 }}>
{t("common.resource")}
</Typography>
{Object.values(groupedByOwner).map(
({ owner, visibleCals, alreadyExisting }) => (
<Box key={owner.displayName} mb={2}>
{visibleCals.length > 0 ? (
visibleCals.map((cal) =>
cal.cal ? (
<CalendarItem
key={cal.owner.displayName + cal.cal["dav:name"]}
cal={cal}
onRemove={() => onRemove(cal)}
onColorChange={(color) => onColorChange(cal, color)}
/>
) : (
<Typography
key={t("calendar.noPublicCalendarsFor", {
name: owner.displayName,
})}
color="textSecondary"
>
{t("calendar.noPublicCalendarsFor", {
name: owner.displayName,
})}
</Typography>
)
)
) : alreadyExisting ? (
<Typography
key={t("calendar.noMoreCalendarsFor", {
name: owner.displayName,
})}
color="textSecondary"
>
{t("calendar.noMoreCalendarsFor", { name: owner.displayName })}
</Typography>
) : null}
</Box>
)
)}
</Box>
);
}
export default function CalendarResources({
open,
onClose,
}: {
open: boolean;
onClose: (ids?: string[]) => void;
}) {
const dispatch = useAppDispatch();
const theme = useTheme();
const openpaasId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const calendars = useAppSelector((state) => state.calendars.list);
const [selectedCal, setSelectedCalendars] = useState<CalendarWithOwner[]>([]);
const [selectedResources, setSelectedResources] = useState<Resource[]>([]);
const fetchSeqRef = useRef(0);
const handleSave = async () => {
if (selectedCal.length > 0) {
const results = await Promise.allSettled(
selectedCal.map(async (cal) => {
const calId = crypto.randomUUID();
const exists = Object.values(calendars).some(
(existing: Calendar) =>
existing.id ===
cal.cal?._links?.self?.href
?.replace("/calendars/", "")
.replace(".json", "")
);
if (!exists && cal.cal) {
await dispatch(
addCalendarResourceAsync({
userId: openpaasId,
calId,
cal: {
...cal,
color: cal.cal["apple:color"]
? {
light: cal.cal["apple:color"],
dark: getAccessiblePair(cal.cal["apple:color"], theme),
}
: defaultColors[0],
},
})
).unwrap();
return cal.cal._links.self?.href
?.replace("/calendars/", "")
.replace(".json", "");
}
return null;
})
);
const idList = results
.filter((r) => r.status === "fulfilled")
.map((r) => (r as PromiseFulfilledResult<string | null>).value)
.filter(Boolean) as string[];
onClose(idList);
} else {
onClose();
}
setSelectedCalendars([]);
setSelectedResources([]);
};
const { t } = useI18n();
const handleClose = () => {
fetchSeqRef.current += 1; // invalidate in-flight fetch results
onClose();
setSelectedCalendars([]);
setSelectedResources([]);
};
return (
<ResponsiveDialog
open={open}
contentSx={{ paddingTop: "8px !important" }}
onClose={handleClose}
title={t("calendar.browseResources")}
actions={
<>
<Button variant="outlined" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button variant="contained" onClick={handleSave}>
{t("actions.add")}
</Button>
</>
}
>
<ResourceSearch
objectTypes={["resource"]}
selectedResources={selectedResources}
inputSlot={(params) => <TextField {...params} size="small" />}
onChange={async (_event: React.SyntheticEvent, value: Resource[]) => {
const requestSeq = ++fetchSeqRef.current;
setSelectedResources(value);
const results = await Promise.allSettled(
value.map(async (user: Resource) => {
if (user?.openpaasId) {
const cals = await getCalendars(
user.openpaasId,
"sharedPublic=true&"
);
return cals._embedded?.["dav:calendar"]
? cals._embedded["dav:calendar"].map((cal) => ({
cal,
owner: user,
}))
: { cal: undefined, owner: user };
}
return null;
})
);
const successfulCals = results
.filter((result) => result.status === "fulfilled")
.map((result) => (result as PromiseFulfilledResult<unknown>).value)
.flat()
.filter(Boolean);
if (requestSeq !== fetchSeqRef.current) return;
setSelectedCalendars(successfulCals as CalendarWithOwner[]);
}}
/>
<SelectedCalendarsList
calendars={calendars}
selectedCal={selectedCal}
onRemove={(cal) => {
if (!cal.cal?._links?.self?.href) return;
setSelectedCalendars((prev) =>
prev.filter(
(c) => c.cal?._links?.self?.href !== cal.cal._links.self?.href
)
);
if (
!selectedCal.find(
(c) =>
cal.owner.displayName === c.owner.displayName &&
c.cal?._links?.self?.href !== cal.cal._links.self?.href
)
) {
setSelectedResources((prev) =>
prev.filter((u) => u.displayName !== cal.owner.displayName)
);
}
}}
onColorChange={(cal, color) =>
setSelectedCalendars((prev) =>
prev.map((prevcal) =>
prevcal.owner.displayName === cal.owner.displayName &&
prevcal.cal._links.self?.href === cal.cal._links.self?.href
? {
...prevcal,
cal: {
...prevcal.cal,
"apple:color": color.light,
},
}
: prevcal
)
)
}
/>
</ResponsiveDialog>
);
}
+18 -2
View File
@@ -25,6 +25,7 @@ import CalendarPopover from "./CalendarModal";
import CalendarSearch from "./CalendarSearch";
import { DeleteCalendarDialog } from "./DeleteCalendarDialog";
import { OwnerCaption } from "./OwnerCaption";
import CalendarResources from "./CalendarResources";
function CalendarAccordion({
title,
@@ -161,7 +162,8 @@ export default function CalendarSelection({
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
const [anchorElCalOthers, setAnchorElCalOthers] =
useState<HTMLElement | null>(null);
// const [anchorElCalResources, setAnchorElCalResources] = useState<HTMLElement | null>(null);
const [anchorElCalResources, setAnchorElCalResources] =
useState<HTMLElement | null>(null);
return (
<>
@@ -212,7 +214,10 @@ export default function CalendarSelection({
title={t("calendar.resources")}
calendars={resourceCalendars}
selectedCalendars={selectedCalendars}
showAddButton={false}
onAddClick={() => {
setAnchorElCalResources(document.body);
}}
showAddButton
handleToggle={handleCalendarToggle}
setOpen={(_) => {
// TO DO: Implement open resource selection
@@ -238,6 +243,17 @@ export default function CalendarSelection({
}
}}
/>
<CalendarResources
open={Boolean(anchorElCalResources)}
onClose={(newResourceIds?: string[]) => {
setAnchorElCalResources(null);
if (newResourceIds?.length) {
newResourceIds.forEach((id) => {
handleCalendarToggle(id);
});
}
}}
/>
</>
);
}