Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/account/select-users.tsx
Views: 687
/*1SelectUsers of this Cocalc server.23Inspired by https://ant.design/components/select/#components-select-demo-select-users4*/56import { ReactNode, useState, useRef, useMemo } from "react";7import { Alert, Select, Spin } from "antd";8import { SelectProps } from "antd/es/select";9import debounce from "lodash/debounce";10import apiPost from "lib/api/post";11import type { User } from "@cocalc/server/accounts/search";12import Timestamp from "components/misc/timestamp";13import Avatar from "components/account/avatar";14import { Icon } from "@cocalc/frontend/components/icon";1516interface Props {17exclude?: string[]; // account_ids to exclude from search18onChange?: (account_ids: string[]) => void;19autoFocus?: boolean;20}2122export default function SelectUsers({ autoFocus, exclude, onChange }: Props) {23const [value, setValue] = useState<UserValue[]>([]);2425return (26<DebounceSelect27autoFocus={autoFocus}28exclude={new Set(exclude)}29mode="multiple"30value={value}31placeholder={"Email address, name or @username"}32fetchOptions={fetchUserList}33onChange={(newValue) => {34setValue(newValue);35onChange?.(newValue.map((x) => x.value));36}}37style={{ width: "100%" }}38/>39);40}4142interface DebounceSelectProps43extends Omit<SelectProps<any>, "options" | "children"> {44fetchOptions: (search: string, exclude?: Set<string>) => Promise<any[]>;45debounceTimeout?: number;46exclude?: Set<string>;47}4849function DebounceSelect({50exclude,51fetchOptions,52debounceTimeout = 800,53...props54}: DebounceSelectProps) {55const [fetching, setFetching] = useState(false);56const [error, setError] = useState<string>("");57const [options, setOptions] = useState<any[]>([]);58const fetchRef = useRef(0);5960const debounceFetcher = useMemo(() => {61const loadOptions = async (value: string) => {62fetchRef.current += 1;63const fetchId = fetchRef.current;64setError("");65setFetching(true);6667try {68const newOptions = await fetchOptions(value, exclude);69if (fetchId == fetchRef.current) {70setOptions(newOptions);71}72} catch (err) {73setError(err.message);74} finally {75setFetching(false);76}77};7879return debounce(loadOptions, debounceTimeout);80}, [fetchOptions, debounceTimeout]);8182return (83<>84{error && (85<Alert type="error" message={error} style={{ marginBottom: "15px" }} />86)}87<Select88labelInValue89filterOption={false}90onSearch={debounceFetcher}91notFoundContent={fetching ? <Spin size="small" /> : null}92{...props}93options={options}94/>95</>96);97}9899interface UserValue {100label: ReactNode;101value: string;102}103104async function fetchUserList(105query: string,106exclude?: Set<string>107): Promise<UserValue[]> {108const v: User[] = await apiPost("/accounts/search", { query });109const list: UserValue[] = [];110for (const user of v) {111if (exclude?.has(user.account_id)) continue;112list.push({113label: <Label {...user} />,114value: user.account_id,115});116}117return list;118}119120function Label({121account_id,122first_name,123last_name,124last_active,125created,126name,127email_address_verified,128}: User) {129return (130<div style={{ borderBottom: "1px solid lightgrey", paddingBottom: "5px" }}>131<Avatar132account_id={account_id}133size={18}134style={{ marginRight: "5px" }}135zIndex={10000}136/>137{first_name} {last_name}138{name ? ` (@${name})` : ""}139{last_active && (140<div>141Last Active: <Timestamp epoch={last_active} dateOnly />142</div>143)}144{created && (145<div>146Created: <Timestamp epoch={created} dateOnly />147</div>148)}149{email_address_verified && (150<div>151<Icon name="check" style={{ color: "darkgreen" }} /> Email address is152verified153</div>154)}155</div>156);157}158159160