ability to register a shared calendar (#118)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-09-22 20:45:02 +02:00
committed by GitHub
parent 8122772bec
commit cd724a30a0
12 changed files with 721 additions and 136 deletions
+37 -85
View File
@@ -1,21 +1,7 @@
import {
Autocomplete,
Avatar,
CircularProgress,
ListItem,
ListItemAvatar,
ListItemText,
TextField,
} from "@mui/material";
import { useEffect, useState } from "react";
import { searchUsers } from "../../features/User/userAPI";
import { userAttendee } from "../../features/User/userDataTypes";
interface User {
email: string;
displayName: string;
avatarUrl: string;
}
import { PeopleSearch, User } from "./PeopleSearch";
export default function UserSearch({
attendees,
@@ -26,77 +12,43 @@ export default function UserSearch({
setAttendees: Function;
disabled?: boolean;
}) {
const [query, setQuery] = useState("");
const [options, setOptions] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [selectedUsers, setSelectedUsers] = useState(
attendees.map((a) => ({
email: a.cal_address,
displayName: a.cn ?? "",
avatarUrl: "",
openpaasId: "",
}))
);
useEffect(() => {
const delayDebounceFn = setTimeout(async () => {
if (query) {
setLoading(true);
const res = await searchUsers(query);
setOptions(res);
setLoading(false);
}
}, 300);
return () => clearTimeout(delayDebounceFn);
}, [query]);
setSelectedUsers(
attendees.map((a) => ({
email: a.cal_address,
displayName: a.cn ?? "",
avatarUrl: "",
openpaasId: "",
}))
);
}, [attendees]);
return (
<Autocomplete
multiple
options={options}
disabled={disabled}
loading={loading}
filterOptions={(x) => x}
fullWidth
getOptionLabel={(option) => option.displayName || option.email}
filterSelectedOptions
onInputChange={(event, value) => setQuery(value)}
onChange={(event, value) =>
setAttendees(
value.map((a) => ({
cn: a.displayName,
cal_address: a.email,
partstat: "NEED_ACTION",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
}))
)
}
renderInput={(params) => (
<TextField
{...params}
label="Search user"
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
renderOption={(props, option) => {
if (attendees.find((a) => a.cal_address === option.email)) return;
return (
<ListItem {...props} key={option.email} disableGutters>
<ListItemAvatar>
<Avatar src={option.avatarUrl} alt={option.displayName} />
</ListItemAvatar>
<ListItemText
primary={option.displayName}
secondary={option.email}
/>
</ListItem>
);
}}
/>
<>
<PeopleSearch
selectedUsers={selectedUsers}
disabled={disabled}
onChange={(event: any, value: User[]) => {
setAttendees(
value.map((a: User) => ({
cn: a.displayName,
cal_address: a.email,
partstat: "NEED_ACTION",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
}))
);
setSelectedUsers(value);
}}
/>
</>
);
}
+106
View File
@@ -0,0 +1,106 @@
import CloseIcon from "@mui/icons-material/Close";
import Autocomplete from "@mui/material/Autocomplete";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import CardHeader from "@mui/material/CardHeader";
import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import ListItem from "@mui/material/ListItem";
import ListItemAvatar from "@mui/material/ListItemAvatar";
import ListItemText from "@mui/material/ListItemText";
import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useState, useEffect } from "react";
import { searchUsers } from "../../features/User/userAPI";
export interface User {
email: string;
displayName: string;
avatarUrl: string;
openpaasId: string;
}
export function PeopleSearch({
selectedUsers,
onChange,
disabled,
}: {
selectedUsers: User[];
onChange: Function;
disabled?: boolean;
}) {
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<User[]>([]);
useEffect(() => {
const delayDebounceFn = setTimeout(async () => {
if (query) {
setLoading(true);
const res = await searchUsers(query, ["user"]);
setOptions(res);
setLoading(false);
}
}, 300);
return () => clearTimeout(delayDebounceFn);
}, [query]);
return (
<Autocomplete
multiple
options={options}
disabled={disabled}
loading={loading}
filterOptions={(x) => x}
fullWidth
getOptionLabel={(option) => option.displayName || option.email}
filterSelectedOptions
value={selectedUsers}
onInputChange={(event, value) => setQuery(value)}
onChange={(event, value) => onChange(event, value)}
renderInput={(params) => (
<TextField
{...params}
label="Search user"
slotProps={{
input: {
...params.InputProps,
endAdornment: (
<>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</>
),
},
}}
/>
)}
renderOption={(props, option) => {
if (selectedUsers.find((u) => u.email === option.email)) return;
return (
<ListItem
{...props}
key={option.email + option.displayName}
disableGutters
>
<ListItemAvatar>
<Avatar src={option.avatarUrl} alt={option.displayName} />
</ListItemAvatar>
<ListItemText
primary={option.displayName}
secondary={option.email}
/>
</ListItem>
);
}}
/>
);
}