56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
import Combobox from "./ui/Combobox";
|
|
|
|
// Pick one registered user by name or email: the download quota's "any user" picker and the share
|
|
// dialog's recipient. Both used to hand-roll an input over an absolute list that only a mouse could
|
|
// reach (no arrow keys, no listbox for a screen reader, closed by a blur timer); this is the shared
|
|
// Combobox in single-select, which brings all of that. A pick is an ACTION here, not a value to show,
|
|
// so nothing stays selected: the field clears for the next one.
|
|
export interface PickableUser {
|
|
id: number;
|
|
email: string;
|
|
display_name?: string | null;
|
|
}
|
|
|
|
export default function UserPicker<U extends PickableUser>({
|
|
users,
|
|
onPick,
|
|
label,
|
|
placeholder,
|
|
loading,
|
|
}: {
|
|
users: readonly U[];
|
|
onPick: (u: U) => void;
|
|
label: string;
|
|
placeholder: string;
|
|
loading?: boolean;
|
|
}) {
|
|
const byId = new Map(users.map((u) => [String(u.id), u]));
|
|
return (
|
|
<Combobox
|
|
label={label}
|
|
placeholder={placeholder}
|
|
multiple={false}
|
|
loading={loading}
|
|
selected={[]}
|
|
// The label carries both, so the client filter matches a name OR an email.
|
|
options={users.map((u) => ({
|
|
value: String(u.id),
|
|
label: u.display_name ? `${u.display_name} · ${u.email}` : u.email,
|
|
}))}
|
|
onChange={(next) => {
|
|
const u = next[0] != null ? byId.get(next[0]) : undefined;
|
|
if (u) onPick(u);
|
|
}}
|
|
renderOption={(o) => {
|
|
const u = byId.get(o.value);
|
|
return (
|
|
<span className="flex min-w-0 flex-col">
|
|
<span className="truncate">{u?.display_name || u?.email}</span>
|
|
{u?.display_name && <span className="truncate text-xs text-muted">{u.email}</span>}
|
|
</span>
|
|
);
|
|
}}
|
|
/>
|
|
);
|
|
}
|