CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/account/select-users.tsx
Views: 687
1
/*
2
SelectUsers of this Cocalc server.
3
4
Inspired by https://ant.design/components/select/#components-select-demo-select-users
5
*/
6
7
import { ReactNode, useState, useRef, useMemo } from "react";
8
import { Alert, Select, Spin } from "antd";
9
import { SelectProps } from "antd/es/select";
10
import debounce from "lodash/debounce";
11
import apiPost from "lib/api/post";
12
import type { User } from "@cocalc/server/accounts/search";
13
import Timestamp from "components/misc/timestamp";
14
import Avatar from "components/account/avatar";
15
import { Icon } from "@cocalc/frontend/components/icon";
16
17
interface Props {
18
exclude?: string[]; // account_ids to exclude from search
19
onChange?: (account_ids: string[]) => void;
20
autoFocus?: boolean;
21
}
22
23
export default function SelectUsers({ autoFocus, exclude, onChange }: Props) {
24
const [value, setValue] = useState<UserValue[]>([]);
25
26
return (
27
<DebounceSelect
28
autoFocus={autoFocus}
29
exclude={new Set(exclude)}
30
mode="multiple"
31
value={value}
32
placeholder={"Email address, name or @username"}
33
fetchOptions={fetchUserList}
34
onChange={(newValue) => {
35
setValue(newValue);
36
onChange?.(newValue.map((x) => x.value));
37
}}
38
style={{ width: "100%" }}
39
/>
40
);
41
}
42
43
interface DebounceSelectProps
44
extends Omit<SelectProps<any>, "options" | "children"> {
45
fetchOptions: (search: string, exclude?: Set<string>) => Promise<any[]>;
46
debounceTimeout?: number;
47
exclude?: Set<string>;
48
}
49
50
function DebounceSelect({
51
exclude,
52
fetchOptions,
53
debounceTimeout = 800,
54
...props
55
}: DebounceSelectProps) {
56
const [fetching, setFetching] = useState(false);
57
const [error, setError] = useState<string>("");
58
const [options, setOptions] = useState<any[]>([]);
59
const fetchRef = useRef(0);
60
61
const debounceFetcher = useMemo(() => {
62
const loadOptions = async (value: string) => {
63
fetchRef.current += 1;
64
const fetchId = fetchRef.current;
65
setError("");
66
setFetching(true);
67
68
try {
69
const newOptions = await fetchOptions(value, exclude);
70
if (fetchId == fetchRef.current) {
71
setOptions(newOptions);
72
}
73
} catch (err) {
74
setError(err.message);
75
} finally {
76
setFetching(false);
77
}
78
};
79
80
return debounce(loadOptions, debounceTimeout);
81
}, [fetchOptions, debounceTimeout]);
82
83
return (
84
<>
85
{error && (
86
<Alert type="error" message={error} style={{ marginBottom: "15px" }} />
87
)}
88
<Select
89
labelInValue
90
filterOption={false}
91
onSearch={debounceFetcher}
92
notFoundContent={fetching ? <Spin size="small" /> : null}
93
{...props}
94
options={options}
95
/>
96
</>
97
);
98
}
99
100
interface UserValue {
101
label: ReactNode;
102
value: string;
103
}
104
105
async function fetchUserList(
106
query: string,
107
exclude?: Set<string>
108
): Promise<UserValue[]> {
109
const v: User[] = await apiPost("/accounts/search", { query });
110
const list: UserValue[] = [];
111
for (const user of v) {
112
if (exclude?.has(user.account_id)) continue;
113
list.push({
114
label: <Label {...user} />,
115
value: user.account_id,
116
});
117
}
118
return list;
119
}
120
121
function Label({
122
account_id,
123
first_name,
124
last_name,
125
last_active,
126
created,
127
name,
128
email_address_verified,
129
}: User) {
130
return (
131
<div style={{ borderBottom: "1px solid lightgrey", paddingBottom: "5px" }}>
132
<Avatar
133
account_id={account_id}
134
size={18}
135
style={{ marginRight: "5px" }}
136
zIndex={10000}
137
/>
138
{first_name} {last_name}
139
{name ? ` (@${name})` : ""}
140
{last_active && (
141
<div>
142
Last Active: <Timestamp epoch={last_active} dateOnly />
143
</div>
144
)}
145
{created && (
146
<div>
147
Created: <Timestamp epoch={created} dateOnly />
148
</div>
149
)}
150
{email_address_verified && (
151
<div>
152
<Icon name="check" style={{ color: "darkgreen" }} /> Email address is
153
verified
154
</div>
155
)}
156
</div>
157
);
158
}
159
160