allow external attendees (#141)

* [#122] added external attendees

* fixup! [#122] added external attendees

* fixup! [#122] added external attendees

* Update src/components/Attendees/AttendeeSearch.tsx

---------

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
Co-authored-by: Benoit TELLIER <btellier@linagora.com>
This commit is contained in:
Camille Moussu
2025-09-25 11:25:22 +02:00
committed by GitHub
parent 2f7cf9c797
commit 2c78a037a9
5 changed files with 56 additions and 13 deletions
@@ -27,6 +27,7 @@ describe("PeopleSearch", () => {
const onChange = jest.fn(); const onChange = jest.fn();
renderWithProviders( renderWithProviders(
<PeopleSearch <PeopleSearch
objectTypes={["user"]}
selectedUsers={selectedUsers} selectedUsers={selectedUsers}
onChange={onChange} onChange={onChange}
{...props} {...props}
@@ -34,6 +34,7 @@ export default function UserSearch({
<> <>
<PeopleSearch <PeopleSearch
selectedUsers={selectedUsers} selectedUsers={selectedUsers}
objectTypes={["user", "contact"]}
disabled={disabled} disabled={disabled}
onChange={(event: any, value: User[]) => { onChange={(event: any, value: User[]) => {
setAttendees( setAttendees(
@@ -48,6 +49,7 @@ export default function UserSearch({
); );
setSelectedUsers(value); setSelectedUsers(value);
}} }}
freeSolo
/> />
</> </>
); );
+51 -13
View File
@@ -32,24 +32,32 @@ export interface User {
export function PeopleSearch({ export function PeopleSearch({
selectedUsers, selectedUsers,
onChange, onChange,
objectTypes,
disabled, disabled,
freeSolo,
onToggleEventPreview, onToggleEventPreview,
}: { }: {
selectedUsers: User[]; selectedUsers: User[];
onChange: Function; onChange: Function;
objectTypes: string[];
disabled?: boolean; disabled?: boolean;
freeSolo?: boolean;
onToggleEventPreview?: Function; onToggleEventPreview?: Function;
}) { }) {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<User[]>([]); const [options, setOptions] = useState<User[]>([]);
const isValidEmail = (email: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const [error, setError] = useState<string | null>(null);
const theme = useTheme(); const theme = useTheme();
useEffect(() => { useEffect(() => {
const delayDebounceFn = setTimeout(async () => { const delayDebounceFn = setTimeout(async () => {
if (query) { if (query) {
setLoading(true); setLoading(true);
const res = await searchUsers(query, ["user"]); const res = await searchUsers(query, objectTypes);
setOptions(res); setOptions(res);
setLoading(false); setLoading(false);
} }
@@ -60,20 +68,40 @@ export function PeopleSearch({
return ( return (
<Autocomplete <Autocomplete
freeSolo={freeSolo}
multiple multiple
options={options} options={options}
disabled={disabled} disabled={disabled}
loading={loading} loading={loading}
filterOptions={(x) => x} filterOptions={(x) => x}
fullWidth fullWidth
getOptionLabel={(option) => option.displayName || option.email} getOptionLabel={(option) => {
if (typeof option === "object") {
return option.displayName || option.email;
} else {
return option;
}
}}
filterSelectedOptions filterSelectedOptions
value={selectedUsers} value={selectedUsers}
onInputChange={(event, value) => setQuery(value)} onInputChange={(event, value) => setQuery(value)}
onChange={(event, value) => onChange(event, value)} onChange={(event, value) => {
const last = value[value.length - 1];
if (typeof last === "string" && !isValidEmail(last)) {
setError(`"${last}" is not a valid email address`);
return;
}
setError(null);
const mapped = value.map((v: any) =>
typeof v === "string" ? { email: v } : v
);
onChange(event, mapped);
}}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
error={!!error}
helperText={error}
placeholder="Search user" placeholder="Search user"
label="Search user" label="Search user"
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -124,16 +152,26 @@ export function PeopleSearch({
); );
}} }}
renderValue={(value, getTagProps) => renderValue={(value, getTagProps) =>
value.map((option, index) => ( value.map((option, index) => {
<Chip const isString = typeof option === "string";
{...getTagProps({ index })} const label = isString ? option : option.displayName || option.email;
sx={{ const chipColor = isString
backgroundColor: option.color, ? theme.palette.grey[300]
color: theme.palette.getContrastText(option.color ?? "#ffffffff"), : (option.color ?? theme.palette.grey[300]);
}} const textColor = theme.palette.getContrastText(chipColor);
label={option.displayName}
/> return (
)) <Chip
{...getTagProps({ index })}
key={label}
sx={{
backgroundColor: chipColor,
color: textColor,
}}
label={label}
/>
);
})
} }
/> />
); );
@@ -259,6 +259,7 @@ export default function CalendarSearch({
<CardHeader title="Browse other calendars" sx={{ pb: 0 }} /> <CardHeader title="Browse other calendars" sx={{ pb: 0 }} />
<CardContent sx={{ flex: 1, overflow: "auto" }}> <CardContent sx={{ flex: 1, overflow: "auto" }}>
<PeopleSearch <PeopleSearch
objectTypes={["user"]}
selectedUsers={selectedUsers} selectedUsers={selectedUsers}
onChange={async (event: any, value: User[]) => { onChange={async (event: any, value: User[]) => {
setSelectedUsers(value); setSelectedUsers(value);
@@ -105,6 +105,7 @@ export function TempCalendarsInput({
return ( return (
<PeopleSearch <PeopleSearch
objectTypes={["user"]}
selectedUsers={tempUsers} selectedUsers={tempUsers}
onChange={handleUserChange} onChange={handleUserChange}
onToggleEventPreview={handleToggleEventPreview} onToggleEventPreview={handleToggleEventPreview}