@arbidocs/blocks 0.5.36 → 0.5.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +22 -0
  2. package/dist/{WorkspaceTableView-ATZHON74.cjs → WorkspaceTableView-KG546U7M.cjs} +5 -5
  3. package/dist/{WorkspaceTableView-ATZHON74.cjs.map → WorkspaceTableView-KG546U7M.cjs.map} +1 -1
  4. package/dist/{WorkspaceTableView-7APDRIPL.js → WorkspaceTableView-OSFIWJ5C.js} +3 -3
  5. package/dist/{WorkspaceTableView-7APDRIPL.js.map → WorkspaceTableView-OSFIWJ5C.js.map} +1 -1
  6. package/dist/{chunk-RBHVIVWP.js → chunk-K6W2GWCN.js} +80 -80
  7. package/dist/chunk-K6W2GWCN.js.map +1 -0
  8. package/dist/{chunk-Q7BS7EOU.cjs → chunk-KFQ6E6LE.cjs} +32 -28
  9. package/dist/chunk-KFQ6E6LE.cjs.map +1 -0
  10. package/dist/{chunk-XHW35TC7.js → chunk-PPHUOWCJ.js} +33 -29
  11. package/dist/chunk-PPHUOWCJ.js.map +1 -0
  12. package/dist/chunk-PTYERKEU.js +2132 -0
  13. package/dist/chunk-PTYERKEU.js.map +1 -0
  14. package/dist/chunk-TQSU3I4C.cjs +2180 -0
  15. package/dist/chunk-TQSU3I4C.cjs.map +1 -0
  16. package/dist/{chunk-G4VUVOHZ.cjs → chunk-YDY4GIC2.cjs} +79 -79
  17. package/dist/chunk-YDY4GIC2.cjs.map +1 -0
  18. package/dist/connect/index.cjs +2509 -1744
  19. package/dist/connect/index.cjs.map +1 -1
  20. package/dist/connect/index.d.ts +2 -0
  21. package/dist/connect/index.js +2221 -1459
  22. package/dist/connect/index.js.map +1 -1
  23. package/dist/connection/databases/ConnectedDatabasesPage.d.ts +1 -0
  24. package/dist/connection/databases/DatabaseViewsMenu.d.ts +10 -0
  25. package/dist/connection/databases/index.d.ts +1 -0
  26. package/dist/connection/databases/useDatabaseCellSources.d.ts +6 -0
  27. package/dist/connection/workspace-form/ConnectedProjectSelector.d.ts +15 -0
  28. package/dist/connection/workspace-form/ConnectedWorkspaceFormModal.d.ts +11 -0
  29. package/dist/connection/workspace-form/WorkspaceCreateForm.d.ts +15 -0
  30. package/dist/connection/workspace-form/index.d.ts +3 -0
  31. package/dist/connection/workspace-form/types.d.ts +28 -0
  32. package/dist/connection/workspace-list/ConnectedWorkspaceList.d.ts +4 -1
  33. package/dist/connection/workspace-list/useConnectedWorkspaceList.d.ts +12 -36
  34. package/dist/databases/index.cjs +193 -2173
  35. package/dist/databases/index.cjs.map +1 -1
  36. package/dist/databases/index.js +4 -2129
  37. package/dist/databases/index.js.map +1 -1
  38. package/dist/workspaces/WorkspaceFormParts.d.ts +3 -2
  39. package/dist/workspaces/index.cjs +27 -27
  40. package/dist/workspaces/index.js +2 -2
  41. package/node_modules/@arbidocs/coedit/package.json +1 -1
  42. package/package.json +5 -5
  43. package/dist/chunk-G4VUVOHZ.cjs.map +0 -1
  44. package/dist/chunk-Q7BS7EOU.cjs.map +0 -1
  45. package/dist/chunk-RBHVIVWP.js.map +0 -1
  46. package/dist/chunk-XHW35TC7.js.map +0 -1
@@ -1,2134 +1,9 @@
1
1
  "use client";
2
- import { DetailPanel, DetailSection, DetailFields, DetailField, DetailLinkList, DetailLinkItem, DetailEmpty } from '../chunk-ZCOVDK2E.js';
3
- import { GridView } from '../chunk-7F5Z4K5W.js';
2
+ export { ADD_COLUMN_COL_ID, COLUMN_TYPE_ICONS, COMPUTED_COLUMN_TYPES, DATABASE_COLUMN_TYPES, DATABASE_ROLLUP_OPS, DEFAULT_COLUMN_CONFIG_LABELS, DEFAULT_DATABASE_COLUMN_DIALOG_LABELS, DEFAULT_DATABASE_COLUMN_LABELS, DEFAULT_DATABASE_COLUMN_TYPE_LABELS, DEFAULT_DATABASE_LIST_LABELS, DEFAULT_DATABASE_ROW_DETAIL_LABELS, DEFAULT_DATABASE_TABLE_LABELS, DEFAULT_DATABASE_TOOLBAR_LABELS, DEFAULT_NEW_DATABASE_DIALOG_LABELS, DEFAULT_PICK_CELL_EDITOR_LABELS, DatabaseColumnDialog, DatabaseList, DatabaseRowDetail, DatabaseTable, DatabaseToolbar, LINK_COLUMN_TYPES, LinkChip, LinkChipsCell, LookupCell, NewDatabaseDialog, OPEN_ROW_COL_ID, PersonCell, PersonChip, PickCellEditor, SelectCellEditor, SelectChip, canEditColumn, cellIds, cellList, columnWidth, compareCells, createDatabaseColumns, formatCell, formatDateValue, formatLookupValue, formatNumberValue, isComputedColumn, optionTone, rowTitle, sameCellValue, titleOf, toApiValue } from '../chunk-PTYERKEU.js';
3
+ import '../chunk-ZCOVDK2E.js';
4
+ import '../chunk-7F5Z4K5W.js';
4
5
  import '../chunk-SLSBJRWW.js';
5
- import { testIdOr } from '../chunk-7F6CBP3Z.js';
6
+ import '../chunk-7F6CBP3Z.js';
6
7
  import '../chunk-VUNV25KB.js';
7
- import { Sigma, Search, ArrowUpRight, FileText, UserRound, ChevronDown, CheckSquare, CalendarDays, Hash, Type, X, Loader2, Check, Maximize2, Plus, Trash2, Rows3, ArrowLeft, FileSpreadsheet, Eye, MoreHorizontal, Upload, Download, Table2, Lock } from 'lucide-react';
8
- import { cn, Button, Input, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DialogFooter, Textarea, Checkbox, Switch, Popover, PopoverTrigger, PopoverContent } from '@arbidocs/react/ui';
9
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
- import { useState, useRef, useEffect, useMemo } from 'react';
11
-
12
- // src/databases/column-types.ts
13
- var DATABASE_COLUMN_TYPES = [
14
- "text",
15
- "number",
16
- "date",
17
- "checkbox",
18
- "select",
19
- "person",
20
- "document",
21
- "relation",
22
- "lookup",
23
- "rollup"
24
- ];
25
- var COMPUTED_COLUMN_TYPES = /* @__PURE__ */ new Set([
26
- "lookup",
27
- "rollup"
28
- ]);
29
- var LINK_COLUMN_TYPES = /* @__PURE__ */ new Set([
30
- "document",
31
- "relation"
32
- ]);
33
- var isComputedColumn = (column) => COMPUTED_COLUMN_TYPES.has(column.type);
34
- var DATABASE_ROLLUP_OPS = ["count", "sum", "avg", "min", "max"];
35
- var DEFAULT_DATABASE_COLUMN_TYPE_LABELS = {
36
- text: "Text",
37
- number: "Number",
38
- date: "Date",
39
- checkbox: "Checkbox",
40
- select: "Select",
41
- person: "Person",
42
- document: "Documents",
43
- relation: "Relation",
44
- lookup: "Lookup",
45
- rollup: "Rollup"
46
- };
47
- var OPTION_TONES = [
48
- "bg-blue-bg text-blue-text",
49
- "bg-green-bg text-green-text",
50
- "bg-yellow-bg text-yellow-text",
51
- "bg-purple-bg text-purple-text",
52
- "bg-red-bg text-red-text",
53
- "bg-gray-bg text-gray-text"
54
- ];
55
- function optionTone(option) {
56
- let hash = 0;
57
- for (const ch of option.toLowerCase()) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
58
- return OPTION_TONES[hash % OPTION_TONES.length];
59
- }
60
- var isEmpty = (value) => value === null || value === void 0 || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
61
- function cellList(value) {
62
- if (isEmpty(value)) return [];
63
- return Array.isArray(value) ? value.filter((v) => !isEmpty(v)) : [value];
64
- }
65
- function cellIds(value) {
66
- return cellList(value).filter((v) => typeof v === "string");
67
- }
68
- function titleOf(id, titles) {
69
- return titles?.[id] ?? id;
70
- }
71
- function sameCellValue(a, b) {
72
- if (Array.isArray(a) || Array.isArray(b)) {
73
- const la = cellList(a);
74
- const lb = cellList(b);
75
- return la.length === lb.length && la.every((v, i) => v === lb[i]);
76
- }
77
- return (a ?? null) === (b ?? null);
78
- }
79
- function formatDateValue(value, locale) {
80
- const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
81
- if (day) {
82
- const [, y, m, d] = day;
83
- return new Date(Date.UTC(+y, +m - 1, +d)).toLocaleDateString(locale, { timeZone: "UTC" });
84
- }
85
- const at = new Date(value);
86
- return Number.isNaN(at.getTime()) ? value : at.toLocaleString(locale);
87
- }
88
- function formatNumberValue(value, locale) {
89
- return value.toLocaleString(locale, { maximumFractionDigits: 6 });
90
- }
91
- var ISO_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
92
- function formatLookupValue(value, titles, locale) {
93
- if (isEmpty(value)) return "";
94
- if (typeof value === "number") return formatNumberValue(value, locale);
95
- if (typeof value === "boolean") return value ? "Yes" : "No";
96
- if (typeof value === "string") {
97
- if (titles?.[value] !== void 0) return titles[value];
98
- if (ISO_DATE.test(value)) return formatDateValue(value, locale);
99
- }
100
- return String(value);
101
- }
102
- var NAMED_TYPES = /* @__PURE__ */ new Set([
103
- "person",
104
- "document",
105
- "relation"
106
- ]);
107
- function formatCell(value, type, locale, titles) {
108
- if (isEmpty(value)) return "";
109
- if ((type === "number" || type === "rollup") && typeof value === "number")
110
- return formatNumberValue(value, locale);
111
- if (type === "date" && typeof value === "string") return formatDateValue(value, locale);
112
- if (type === "checkbox") return value ? "Yes" : "No";
113
- if (NAMED_TYPES.has(type))
114
- return cellIds(value).map((id) => titleOf(id, titles)).join(", ");
115
- if (type === "lookup")
116
- return cellList(value).map((v) => formatLookupValue(v, titles, locale)).join(", ");
117
- return String(value);
118
- }
119
- function toApiValue(value, type) {
120
- if (isEmpty(value)) return null;
121
- if (LINK_COLUMN_TYPES.has(type)) return cellIds(value);
122
- if (type === "checkbox") return Boolean(value);
123
- if (type === "date" && value instanceof Date) {
124
- const pad = (n) => String(n).padStart(2, "0");
125
- return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
126
- }
127
- if (type === "number" && typeof value === "string") {
128
- const n = Number(value.replace(/,/g, ""));
129
- return Number.isFinite(n) ? n : value;
130
- }
131
- return value;
132
- }
133
- function compareCells(a, b, type, titlesA, titlesB) {
134
- const ea = isEmpty(a);
135
- const eb = isEmpty(b);
136
- if (ea || eb) return ea === eb ? 0 : ea ? 1 : -1;
137
- if (type === "number" || type === "checkbox" || type === "rollup") return Number(a) - Number(b);
138
- const shown = NAMED_TYPES.has(type) || type === "lookup";
139
- const ta = shown ? formatCell(a, type, void 0, titlesA) : String(a);
140
- const tb = shown ? formatCell(b, type, void 0, titlesB) : String(b);
141
- return ta.localeCompare(tb, void 0, { sensitivity: "base", numeric: true });
142
- }
143
- function rowTitle(row, columns, locale) {
144
- const first = columns[0];
145
- return first ? formatCell(row.values[first.id], first.type, locale, row.titles) : "";
146
- }
147
- var CHIP = "inline-flex h-[22px] min-w-0 max-w-[14rem] shrink-0 items-center gap-1 rounded-md border border-border bg-muted/50 px-1.5 text-xs text-foreground";
148
- function initials(name) {
149
- const parts = name.trim().split(/\s+/).filter(Boolean);
150
- const letters = parts.length > 1 ? parts[0][0] + parts[parts.length - 1][0] : name.slice(0, 2);
151
- return letters.toUpperCase();
152
- }
153
- function PersonChip({ name }) {
154
- return /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5 text-sm", children: [
155
- /* @__PURE__ */ jsx(
156
- "span",
157
- {
158
- "aria-hidden": true,
159
- className: "flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-muted-foreground",
160
- children: initials(name)
161
- }
162
- ),
163
- /* @__PURE__ */ jsx("span", { className: "truncate", children: name })
164
- ] });
165
- }
166
- function LinkChip({
167
- label,
168
- kind,
169
- onOpen,
170
- testId
171
- }) {
172
- const Icon = kind === "document" ? FileText : ArrowUpRight;
173
- const body = /* @__PURE__ */ jsxs(Fragment, { children: [
174
- /* @__PURE__ */ jsx(Icon, { className: "size-3 shrink-0 text-muted-foreground", "aria-hidden": true }),
175
- /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
176
- ] });
177
- if (!onOpen) {
178
- return /* @__PURE__ */ jsx("span", { className: CHIP, title: label, "data-testid": testId, children: body });
179
- }
180
- const stop = (e) => e.stopPropagation();
181
- return /* @__PURE__ */ jsx(
182
- "button",
183
- {
184
- type: "button",
185
- title: label,
186
- "data-testid": testId,
187
- onClick: (e) => {
188
- e.stopPropagation();
189
- onOpen();
190
- },
191
- onDoubleClick: stop,
192
- onMouseDown: stop,
193
- className: cn(CHIP, "hover:bg-muted hover:underline"),
194
- children: body
195
- }
196
- );
197
- }
198
- function ChipRow({ children, wrap }) {
199
- return /* @__PURE__ */ jsx(
200
- "span",
201
- {
202
- className: cn(
203
- "flex min-w-0 items-center gap-1",
204
- wrap ? "flex-wrap py-0.5" : "h-full overflow-hidden"
205
- ),
206
- children
207
- }
208
- );
209
- }
210
- function PersonCell({ value, titles }) {
211
- const [id] = cellIds(value);
212
- return id ? /* @__PURE__ */ jsx(PersonChip, { name: titleOf(id, titles) }) : null;
213
- }
214
- function LinkChipsCell({
215
- value,
216
- titles,
217
- kind,
218
- onOpenDocument,
219
- wrap
220
- }) {
221
- const ids = cellIds(value);
222
- if (ids.length === 0) return null;
223
- return /* @__PURE__ */ jsx(ChipRow, { wrap, children: ids.map((id) => /* @__PURE__ */ jsx(
224
- LinkChip,
225
- {
226
- label: titleOf(id, titles),
227
- kind,
228
- testId: `database-chip-${id}`,
229
- onOpen: kind === "document" && onOpenDocument ? () => onOpenDocument(id) : void 0
230
- },
231
- id
232
- )) });
233
- }
234
- function LookupCell({
235
- value,
236
- titles,
237
- locale,
238
- wrap
239
- }) {
240
- const values = cellList(value);
241
- if (values.length === 0) return null;
242
- return /* @__PURE__ */ jsx(ChipRow, { wrap, children: values.map((v, i) => {
243
- const text = formatLookupValue(v, titles, locale);
244
- const key = `${i}-${text}`;
245
- if (typeof v === "string" && /^doc-/.test(v))
246
- return /* @__PURE__ */ jsx(LinkChip, { label: text, kind: "document" }, key);
247
- if (typeof v === "string" && /^dbr-/.test(v))
248
- return /* @__PURE__ */ jsx(LinkChip, { label: text, kind: "relation" }, key);
249
- return /* @__PURE__ */ jsx(
250
- "span",
251
- {
252
- title: text,
253
- className: "inline-flex h-[22px] min-w-0 max-w-[14rem] shrink-0 items-center truncate rounded-md bg-muted/60 px-1.5 text-xs text-foreground",
254
- children: text
255
- },
256
- key
257
- );
258
- }) });
259
- }
260
- var MAX_SHOWN = 50;
261
- var SEARCH_DEBOUNCE_MS = 200;
262
- var DEFAULT_PICK_CELL_EDITOR_LABELS = {
263
- searchPlaceholder: "Search\u2026",
264
- noMatches: "No matches",
265
- searching: "Searching\u2026",
266
- clear: "Clear",
267
- done: "Done",
268
- remove: (label) => `Remove ${label}`
269
- };
270
- function useChoices(query, options, search) {
271
- const [found, setFound] = useState([]);
272
- const [loading, setLoading] = useState(Boolean(search));
273
- useEffect(() => {
274
- if (!search) return;
275
- let live = true;
276
- setLoading(true);
277
- const t = setTimeout(
278
- () => {
279
- search(query.trim()).then((rows) => live && setFound(rows)).catch(() => live && setFound([])).finally(() => live && setLoading(false));
280
- },
281
- query ? SEARCH_DEBOUNCE_MS : 0
282
- );
283
- return () => {
284
- live = false;
285
- clearTimeout(t);
286
- };
287
- }, [query, search]);
288
- const filtered = useMemo(() => {
289
- if (search) return found.slice(0, MAX_SHOWN);
290
- const q = query.trim().toLowerCase();
291
- const all = options ?? [];
292
- const hits = q ? all.filter(
293
- (o) => o.label.toLowerCase().includes(q) || (o.description ?? "").toLowerCase().includes(q)
294
- ) : all;
295
- return hits.slice(0, MAX_SHOWN);
296
- }, [found, options, query, search]);
297
- return { choices: filtered, loading: Boolean(search) && loading };
298
- }
299
- function PickCellEditor(props) {
300
- const { value, onValueChange, stopEditing, data, kind, options, search } = props;
301
- const l = { ...DEFAULT_PICK_CELL_EDITOR_LABELS, ...props.labels };
302
- const multiple = kind !== "person";
303
- const [chosen, setChosen] = useState(() => cellIds(value));
304
- const [query, setQuery] = useState("");
305
- const [active, setActive] = useState(0);
306
- const input = useRef(null);
307
- const { choices, loading } = useChoices(query, options, search);
308
- const names = useRef({ ...data?.titles ?? {} });
309
- for (const o of options ?? []) names.current[o.id] ??= o.label;
310
- for (const o of choices) names.current[o.id] = o.label;
311
- const nameOf = (id) => names.current[id] ?? id;
312
- useEffect(() => input.current?.focus(), []);
313
- useEffect(() => setActive(0), [query]);
314
- const commit = (ids) => {
315
- setChosen(ids);
316
- if (data) {
317
- const titles = { ...data.titles ?? {} };
318
- for (const id of ids) titles[id] ??= nameOf(id);
319
- data.titles = titles;
320
- }
321
- onValueChange(multiple ? ids : ids[0] ?? null);
322
- };
323
- const close = () => setTimeout(() => stopEditing(), 0);
324
- const toggle = (id) => {
325
- if (!multiple) {
326
- commit([id]);
327
- close();
328
- return;
329
- }
330
- commit(chosen.includes(id) ? chosen.filter((x) => x !== id) : [...chosen, id]);
331
- };
332
- const onKeyDown = (e) => {
333
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
334
- e.preventDefault();
335
- e.stopPropagation();
336
- if (choices.length === 0) return;
337
- const step = e.key === "ArrowDown" ? 1 : -1;
338
- setActive((i) => (i + step + choices.length) % choices.length);
339
- } else if (e.key === "Enter") {
340
- e.preventDefault();
341
- e.stopPropagation();
342
- const pick = choices[active];
343
- if (pick) toggle(pick.id);
344
- else if (multiple) close();
345
- } else if (e.key === "Backspace" && multiple && !query && chosen.length > 0) {
346
- commit(chosen.slice(0, -1));
347
- } else if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "Home" || e.key === "End") {
348
- e.stopPropagation();
349
- }
350
- };
351
- return /* @__PURE__ */ jsxs(
352
- "div",
353
- {
354
- "data-testid": "database-pick-editor",
355
- className: "flex w-72 flex-col rounded-md border border-border bg-popover shadow-md",
356
- children: [
357
- multiple && chosen.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 border-b border-border p-1.5", children: chosen.map((id) => /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center", children: [
358
- /* @__PURE__ */ jsx(LinkChip, { label: nameOf(id), kind: kind === "document" ? "document" : "relation" }),
359
- /* @__PURE__ */ jsx(
360
- "button",
361
- {
362
- type: "button",
363
- "aria-label": l.remove(nameOf(id)),
364
- onClick: () => commit(chosen.filter((x) => x !== id)),
365
- "data-testid": `database-pick-remove-${id}`,
366
- className: "rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
367
- children: /* @__PURE__ */ jsx(X, { className: "size-3" })
368
- }
369
- )
370
- ] }, id)) }),
371
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 border-b border-border px-2", children: [
372
- /* @__PURE__ */ jsx(Search, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
373
- /* @__PURE__ */ jsx(
374
- "input",
375
- {
376
- ref: input,
377
- value: query,
378
- placeholder: l.searchPlaceholder,
379
- onChange: (e) => setQuery(e.target.value),
380
- onKeyDown,
381
- "data-testid": "database-pick-search",
382
- className: "h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
383
- }
384
- ),
385
- loading && /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin text-muted-foreground" })
386
- ] }),
387
- /* @__PURE__ */ jsxs("div", { role: "listbox", "aria-multiselectable": multiple, className: "max-h-64 overflow-y-auto p-1", children: [
388
- choices.map((o, i) => {
389
- const isChosen = chosen.includes(o.id);
390
- return /* @__PURE__ */ jsxs(
391
- "button",
392
- {
393
- type: "button",
394
- role: "option",
395
- "aria-selected": isChosen,
396
- onMouseEnter: () => setActive(i),
397
- onClick: () => toggle(o.id),
398
- "data-testid": `database-pick-option-${o.id}`,
399
- className: cn(
400
- "flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm",
401
- i === active && "bg-muted"
402
- ),
403
- children: [
404
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
405
- kind === "person" ? /* @__PURE__ */ jsx(PersonChip, { name: o.label }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: o.label }),
406
- o.description && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: o.description })
407
- ] }),
408
- isChosen && /* @__PURE__ */ jsx(Check, { className: "size-3.5 shrink-0" })
409
- ]
410
- },
411
- o.id
412
- );
413
- }),
414
- choices.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-2 py-1.5 text-sm text-muted-foreground", children: loading ? l.searching : l.noMatches })
415
- ] }),
416
- (multiple || chosen.length > 0) && /* @__PURE__ */ jsx("div", { className: "flex justify-end border-t border-border p-1", children: !multiple ? /* @__PURE__ */ jsx(
417
- "button",
418
- {
419
- type: "button",
420
- onClick: () => {
421
- commit([]);
422
- close();
423
- },
424
- "data-testid": "database-pick-clear",
425
- className: "rounded px-2 py-1 text-sm text-muted-foreground hover:bg-muted",
426
- children: l.clear
427
- }
428
- ) : /* @__PURE__ */ jsx(
429
- "button",
430
- {
431
- type: "button",
432
- onClick: close,
433
- "data-testid": "database-pick-done",
434
- className: "rounded px-2 py-1 text-sm font-medium hover:bg-muted",
435
- children: l.done
436
- }
437
- ) })
438
- ]
439
- }
440
- );
441
- }
442
- function SelectChip({ value }) {
443
- return /* @__PURE__ */ jsx(
444
- "span",
445
- {
446
- className: cn(
447
- "inline-flex max-w-full items-center truncate rounded-md px-2 py-0.5 text-xs font-medium",
448
- optionTone(value)
449
- ),
450
- children: value
451
- }
452
- );
453
- }
454
- function SelectCellEditor(props) {
455
- const { value, onValueChange, stopEditing, options, clearLabel = "Clear" } = props;
456
- const items = [...options, null];
457
- const [active, setActive] = useState(() => Math.max(0, options.indexOf(String(value ?? ""))));
458
- const list = useRef(null);
459
- useEffect(() => list.current?.focus(), []);
460
- const pick = (option) => {
461
- onValueChange(option);
462
- setTimeout(() => stopEditing(), 0);
463
- };
464
- const onKeyDown = (e) => {
465
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
466
- e.preventDefault();
467
- e.stopPropagation();
468
- const step = e.key === "ArrowDown" ? 1 : -1;
469
- setActive((i) => (i + step + items.length) % items.length);
470
- } else if (e.key === "Enter") {
471
- e.preventDefault();
472
- e.stopPropagation();
473
- pick(items[active]);
474
- }
475
- };
476
- return /* @__PURE__ */ jsx(
477
- "div",
478
- {
479
- ref: list,
480
- role: "listbox",
481
- tabIndex: -1,
482
- onKeyDown,
483
- "data-testid": "database-select-editor",
484
- className: "flex min-w-44 flex-col gap-0.5 rounded-md border border-border bg-popover p-1 shadow-md outline-none",
485
- children: items.map((option, i) => /* @__PURE__ */ jsxs(
486
- "button",
487
- {
488
- type: "button",
489
- role: "option",
490
- "aria-selected": option === value,
491
- onMouseEnter: () => setActive(i),
492
- onClick: () => pick(option),
493
- "data-testid": option === null ? "database-select-clear" : `database-select-option-${option}`,
494
- className: cn(
495
- "flex items-center gap-2 rounded px-2 py-1 text-left text-sm",
496
- i === active && "bg-muted"
497
- ),
498
- children: [
499
- option === null ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: clearLabel }) : /* @__PURE__ */ jsx(SelectChip, { value: option }),
500
- option !== null && option === value && /* @__PURE__ */ jsx(Check, { className: "ml-auto size-3.5" })
501
- ]
502
- },
503
- option ?? "__clear"
504
- ))
505
- }
506
- );
507
- }
508
- var DEFAULT_DATABASE_COLUMN_LABELS = {
509
- addColumn: "Add a column",
510
- columnSettings: (name) => `${name} column settings`,
511
- computed: "Computed from a relation (read-only)",
512
- openRow: "Open row"
513
- };
514
- var ADD_COLUMN_COL_ID = "__add_column";
515
- var OPEN_ROW_COL_ID = "__open_row";
516
- var COLUMN_TYPE_ICONS = {
517
- text: Type,
518
- number: Hash,
519
- date: CalendarDays,
520
- checkbox: CheckSquare,
521
- select: ChevronDown,
522
- person: UserRound,
523
- document: FileText,
524
- relation: ArrowUpRight,
525
- lookup: Search,
526
- rollup: Sigma
527
- };
528
- var WIDTHS = {
529
- text: 220,
530
- number: 130,
531
- date: 140,
532
- checkbox: 110,
533
- select: 160,
534
- person: 180,
535
- document: 240,
536
- relation: 240,
537
- lookup: 200,
538
- rollup: 130
539
- };
540
- function HeaderLabel({
541
- column,
542
- onEdit,
543
- label,
544
- computedLabel
545
- }) {
546
- const Icon = COLUMN_TYPE_ICONS[column.type];
547
- return /* @__PURE__ */ jsxs("span", { className: "group/dbh flex min-w-0 items-center gap-1.5", children: [
548
- /* @__PURE__ */ jsx(Icon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
549
- /* @__PURE__ */ jsx("span", { className: "truncate text-[12.5px] font-medium", children: column.name }),
550
- isComputedColumn(column) && /* @__PURE__ */ jsx("span", { title: computedLabel, "data-testid": `database-column-computed-${column.id}`, children: /* @__PURE__ */ jsx(Lock, { className: "size-3 shrink-0 text-muted-foreground/70", "aria-label": computedLabel }) }),
551
- onEdit && /* @__PURE__ */ jsx(
552
- "button",
553
- {
554
- type: "button",
555
- "aria-label": label,
556
- title: label,
557
- "data-testid": `database-column-settings-${column.id}`,
558
- onClick: (e) => {
559
- e.stopPropagation();
560
- onEdit();
561
- },
562
- onMouseDown: (e) => e.stopPropagation(),
563
- className: "ml-auto rounded p-0.5 text-muted-foreground opacity-0 hover:bg-muted hover:text-foreground focus-visible:opacity-100 group-hover/dbh:opacity-100",
564
- children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-3.5" })
565
- }
566
- )
567
- ] });
568
- }
569
- function columnWidth(column) {
570
- const header = Math.round(column.name.length * 8) + 76 + (isComputedColumn(column) ? 16 : 0);
571
- return Math.min(320, Math.max(WIDTHS[column.type], header));
572
- }
573
- function canEditColumn(column, sources) {
574
- if (isComputedColumn(column)) return false;
575
- if (column.type === "person") return Boolean(sources?.members);
576
- if (column.type === "document") return Boolean(sources?.documents);
577
- if (column.type === "relation") return Boolean(sources?.searchRows && column.relation);
578
- return true;
579
- }
580
- function editorFor(column, sources) {
581
- const pick = (params) => ({
582
- cellEditor: PickCellEditor,
583
- cellEditorPopup: true,
584
- cellEditorParams: params
585
- });
586
- switch (column.type) {
587
- case "number":
588
- return { cellEditor: "agNumberCellEditor", cellEditorParams: { showStepperButtons: false } };
589
- case "date":
590
- return { cellEditor: "agDateStringCellEditor" };
591
- case "checkbox":
592
- return { cellEditor: "agCheckboxCellEditor" };
593
- case "select":
594
- return {
595
- cellEditor: SelectCellEditor,
596
- cellEditorPopup: true,
597
- cellEditorParams: { options: column.options ?? [] }
598
- };
599
- case "person":
600
- return pick({ kind: "person", options: sources?.members ?? [] });
601
- case "document":
602
- return pick({ kind: "document", options: sources?.documents ?? [] });
603
- case "relation": {
604
- const target = column.relation?.database_id;
605
- const searchRows = sources?.searchRows;
606
- return pick({
607
- kind: "relation",
608
- search: target && searchRows ? (q) => searchRows(target, q) : void 0
609
- });
610
- }
611
- case "lookup":
612
- case "rollup":
613
- return {};
614
- default:
615
- return { cellEditor: "agTextCellEditor" };
616
- }
617
- }
618
- function rendererFor(column, locale, sources) {
619
- switch (column.type) {
620
- case "checkbox":
621
- return {
622
- cellRenderer: "agCheckboxCellRenderer",
623
- cellClass: "flex items-center justify-center"
624
- };
625
- case "select":
626
- return {
627
- cellRenderer: (p) => typeof p.value === "string" && p.value ? /* @__PURE__ */ jsx(SelectChip, { value: p.value }) : null
628
- };
629
- case "number":
630
- return {
631
- cellClass: "cell-numeric",
632
- valueFormatter: (p) => formatCell(p.value, "number", locale)
633
- };
634
- case "rollup":
635
- return {
636
- cellClass: "cell-numeric text-muted-foreground",
637
- valueFormatter: (p) => formatCell(p.value, "rollup", locale)
638
- };
639
- case "date":
640
- return {
641
- valueFormatter: (p) => formatCell(p.value, "date", locale)
642
- };
643
- case "person":
644
- return {
645
- cellRenderer: (p) => /* @__PURE__ */ jsx(PersonCell, { value: p.value, titles: p.data?.titles })
646
- };
647
- case "document":
648
- case "relation":
649
- return {
650
- cellRenderer: (p) => /* @__PURE__ */ jsx(
651
- LinkChipsCell,
652
- {
653
- value: p.value,
654
- titles: p.data?.titles,
655
- kind: column.type === "document" ? "document" : "relation",
656
- onOpenDocument: sources?.onOpenDocument
657
- }
658
- )
659
- };
660
- case "lookup":
661
- return {
662
- cellRenderer: (p) => /* @__PURE__ */ jsx(LookupCell, { value: p.value, titles: p.data?.titles, locale })
663
- };
664
- default:
665
- return {};
666
- }
667
- }
668
- function createDatabaseColumns(columns, options = {}) {
669
- const { readOnly, onEditColumn, onAddColumn, onOpenRow, locale, sources } = options;
670
- const l = { ...DEFAULT_DATABASE_COLUMN_LABELS, ...options.labels };
671
- const openRow = onOpenRow && columns.length > 0 ? [
672
- {
673
- colId: OPEN_ROW_COL_ID,
674
- headerName: "",
675
- width: 40,
676
- minWidth: 40,
677
- maxWidth: 40,
678
- resizable: false,
679
- sortable: false,
680
- editable: false,
681
- suppressMovable: true,
682
- // Pinned: the button stays in view however far the table scrolls.
683
- pinned: "left",
684
- lockPinned: true,
685
- lockPosition: "left",
686
- cellClass: "flex items-center justify-center !px-0",
687
- valueGetter: () => null,
688
- cellRenderer: (p) => p.data ? /* @__PURE__ */ jsx(
689
- "button",
690
- {
691
- type: "button",
692
- "aria-label": l.openRow,
693
- title: l.openRow,
694
- "data-testid": `database-open-row-${p.data.id}`,
695
- onClick: (e) => {
696
- e.stopPropagation();
697
- if (p.data) onOpenRow(p.data);
698
- },
699
- onDoubleClick: (e) => e.stopPropagation(),
700
- className: "flex size-6 items-center justify-center rounded text-muted-foreground/70 hover:bg-muted hover:text-foreground",
701
- children: /* @__PURE__ */ jsx(Maximize2, { className: "size-3.5" })
702
- }
703
- ) : null
704
- }
705
- ] : [];
706
- const defs = columns.map((column) => ({
707
- colId: column.id,
708
- headerName: column.name,
709
- width: columnWidth(column),
710
- minWidth: 90,
711
- resizable: true,
712
- sortable: true,
713
- editable: !readOnly && canEditColumn(column, sources),
714
- // The grid's own type inference must not second-guess the database's column type.
715
- cellDataType: false,
716
- headerComponentParams: {
717
- innerHeaderComponent: () => /* @__PURE__ */ jsx(
718
- HeaderLabel,
719
- {
720
- column,
721
- label: l.columnSettings(column.name),
722
- computedLabel: l.computed,
723
- onEdit: !readOnly && onEditColumn ? () => onEditColumn(column) : void 0
724
- }
725
- )
726
- },
727
- // An empty checkbox reads as unticked (not indeterminate).
728
- valueGetter: (p) => column.type === "checkbox" ? p.data?.values[column.id] === true : p.data?.values[column.id] ?? null,
729
- // Edits land in the row's values as the API would store them; the host saves them in
730
- // onCellValueChanged and puts the server's normalised row back.
731
- valueSetter: (p) => {
732
- if (!p.data) return false;
733
- const next = toApiValue(p.newValue, column.type);
734
- if (sameCellValue(next, p.oldValue)) return false;
735
- p.data.values = { ...p.data.values, [column.id]: next };
736
- return true;
737
- },
738
- comparator: (a, b, nodeA, nodeB) => compareCells(a, b, column.type, nodeA?.data?.titles, nodeB?.data?.titles),
739
- getQuickFilterText: (p) => formatCell(p.value, column.type, locale, p.data?.titles),
740
- ...editorFor(column, sources),
741
- ...rendererFor(column, locale, sources)
742
- }));
743
- defs.unshift(...openRow);
744
- if (!readOnly && onAddColumn) {
745
- defs.push({
746
- colId: ADD_COLUMN_COL_ID,
747
- width: 52,
748
- minWidth: 52,
749
- maxWidth: 52,
750
- resizable: false,
751
- sortable: false,
752
- editable: false,
753
- suppressMovable: true,
754
- // Pinned: on a wide table the add button stays in view instead of scrolling off the right.
755
- pinned: "right",
756
- lockPinned: true,
757
- headerComponent: () => /* @__PURE__ */ jsx(
758
- "button",
759
- {
760
- type: "button",
761
- "aria-label": l.addColumn,
762
- title: l.addColumn,
763
- "data-testid": "database-add-column",
764
- onClick: onAddColumn,
765
- className: "flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground",
766
- children: /* @__PURE__ */ jsx(Plus, { className: "size-4" })
767
- }
768
- ),
769
- valueGetter: () => null
770
- });
771
- }
772
- return defs;
773
- }
774
- var DEFAULT_COLUMN_CONFIG_LABELS = {
775
- relatedDatabase: "Related database",
776
- pickDatabase: "Pick a database",
777
- noDatabases: "There is no database to link to yet.",
778
- targetFixed: "The related database can\u2019t be changed once the column has been added.",
779
- thisDatabase: "this database",
780
- showOnTarget: (target) => `Show on ${target}`,
781
- showOnTargetHint: "Adds a column to the related database that lists the links back.",
782
- pairName: "Name of the column there",
783
- pairNamePlaceholder: (name) => name,
784
- pairedWith: (target) => `Shown on ${target} too (two-way).`,
785
- oneWay: "One-way: only this database shows the links.",
786
- relation: "Relation",
787
- pickRelation: "Pick a relation column",
788
- noRelations: "Add a relation column first: lookups and rollups read through one.",
789
- lookupColumn: "Column to show",
790
- pickColumn: "Pick a column",
791
- noColumns: "The related database has no column to show.",
792
- rollupOp: "Calculate",
793
- rollupColumn: "Of the number column",
794
- noNumberColumns: "The related database has no number column.",
795
- ops: { count: "Count", sum: "Sum", avg: "Average", min: "Min", max: "Max" }
796
- };
797
- function Field({ label, children, hint }) {
798
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
799
- /* @__PURE__ */ jsx(Label, { children: label }),
800
- children,
801
- hint && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hint })
802
- ] });
803
- }
804
- function Hint({ children, testId }) {
805
- return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "data-testid": testId, children });
806
- }
807
- function ColumnSelect({
808
- value,
809
- onChange,
810
- columns,
811
- placeholder,
812
- testId
813
- }) {
814
- return /* @__PURE__ */ jsxs(Select, { value: value || void 0, onValueChange: onChange, children: [
815
- /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": testId, children: /* @__PURE__ */ jsx(SelectValue, { placeholder }) }),
816
- /* @__PURE__ */ jsx(SelectContent, { children: columns.map((c) => {
817
- const Icon = COLUMN_TYPE_ICONS[c.type];
818
- return /* @__PURE__ */ jsx(SelectItem, { value: c.id, "data-testid": `${testId}-${c.id}`, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
819
- /* @__PURE__ */ jsx(Icon, { className: "size-4 text-muted-foreground" }),
820
- c.name
821
- ] }) }, c.id);
822
- }) })
823
- ] });
824
- }
825
- function RelationFields({
826
- draft,
827
- onChange,
828
- databases,
829
- databaseId,
830
- databaseName,
831
- editing,
832
- column,
833
- labels
834
- }) {
835
- const nameOf = (id) => databases.find((d) => d.id === id)?.name ?? (id === databaseId ? labels.thisDatabase : id);
836
- if (editing && column?.relation) {
837
- const target2 = nameOf(column.relation.database_id);
838
- return /* @__PURE__ */ jsxs(Field, { label: labels.relatedDatabase, hint: labels.targetFixed, children: [
839
- /* @__PURE__ */ jsx("p", { className: "text-sm", "data-testid": "database-column-relation-target", children: target2 }),
840
- /* @__PURE__ */ jsx(Hint, { children: column.relation.two_way ? labels.pairedWith(target2) : labels.oneWay })
841
- ] });
842
- }
843
- if (databases.length === 0) return /* @__PURE__ */ jsx(Hint, { children: labels.noDatabases });
844
- const target = draft.database_id ? nameOf(draft.database_id) : "";
845
- return /* @__PURE__ */ jsxs(Fragment, { children: [
846
- /* @__PURE__ */ jsx(Field, { label: labels.relatedDatabase, children: /* @__PURE__ */ jsxs(
847
- Select,
848
- {
849
- value: draft.database_id || void 0,
850
- onValueChange: (v) => onChange({ ...draft, database_id: v }),
851
- children: [
852
- /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": "database-column-relation-database", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: labels.pickDatabase }) }),
853
- /* @__PURE__ */ jsx(SelectContent, { children: databases.map((d) => /* @__PURE__ */ jsx(
854
- SelectItem,
855
- {
856
- value: d.id,
857
- "data-testid": `database-column-relation-database-${d.id}`,
858
- children: d.id === databaseId ? `${d.name} (${labels.thisDatabase})` : d.name
859
- },
860
- d.id
861
- )) })
862
- ]
863
- }
864
- ) }),
865
- draft.database_id && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-border p-3", children: [
866
- /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-3 text-sm", children: [
867
- /* @__PURE__ */ jsxs("span", { className: "flex flex-col", children: [
868
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: labels.showOnTarget(target) }),
869
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: labels.showOnTargetHint })
870
- ] }),
871
- /* @__PURE__ */ jsx(
872
- Switch,
873
- {
874
- checked: draft.two_way,
875
- onCheckedChange: (two_way) => onChange({ ...draft, two_way }),
876
- "data-testid": "database-column-relation-two-way"
877
- }
878
- )
879
- ] }),
880
- draft.two_way && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
881
- /* @__PURE__ */ jsx(Label, { htmlFor: "database-column-pair-name", children: labels.pairName }),
882
- /* @__PURE__ */ jsx(
883
- Input,
884
- {
885
- id: "database-column-pair-name",
886
- value: draft.pair_name,
887
- maxLength: 200,
888
- placeholder: labels.pairNamePlaceholder(databaseName ?? ""),
889
- onChange: (e) => onChange({ ...draft, pair_name: e.target.value }),
890
- "data-testid": "database-column-pair-name"
891
- }
892
- )
893
- ] })
894
- ] })
895
- ] });
896
- }
897
- function relationColumnsOf(columns) {
898
- return columns.filter((c) => c.type === "relation" && c.relation);
899
- }
900
- function targetColumnsOf(relationId, columns, databases, databaseId) {
901
- const relation = columns.find((c) => c.id === relationId)?.relation;
902
- if (!relation) return [];
903
- if (relation.database_id === databaseId) return columns;
904
- return databases.find((d) => d.id === relation.database_id)?.columns ?? [];
905
- }
906
- function LookupFields({
907
- draft,
908
- onChange,
909
- columns,
910
- databases,
911
- databaseId,
912
- labels
913
- }) {
914
- const relations = relationColumnsOf(columns);
915
- if (relations.length === 0)
916
- return /* @__PURE__ */ jsx(Hint, { testId: "database-column-no-relations", children: labels.noRelations });
917
- const shown = targetColumnsOf(draft.relation, columns, databases, databaseId).filter(
918
- (c) => !isComputedColumn(c)
919
- );
920
- return /* @__PURE__ */ jsxs(Fragment, { children: [
921
- /* @__PURE__ */ jsx(Field, { label: labels.relation, children: /* @__PURE__ */ jsx(
922
- ColumnSelect,
923
- {
924
- value: draft.relation,
925
- onChange: (relation) => onChange({ relation, column: "" }),
926
- columns: relations,
927
- placeholder: labels.pickRelation,
928
- testId: "database-column-lookup-relation"
929
- }
930
- ) }),
931
- draft.relation && /* @__PURE__ */ jsx(Field, { label: labels.lookupColumn, children: shown.length ? /* @__PURE__ */ jsx(
932
- ColumnSelect,
933
- {
934
- value: draft.column,
935
- onChange: (column) => onChange({ ...draft, column }),
936
- columns: shown,
937
- placeholder: labels.pickColumn,
938
- testId: "database-column-lookup-column"
939
- }
940
- ) : /* @__PURE__ */ jsx(Hint, { children: labels.noColumns }) })
941
- ] });
942
- }
943
- function RollupFields({
944
- draft,
945
- onChange,
946
- columns,
947
- databases,
948
- databaseId,
949
- labels
950
- }) {
951
- const relations = relationColumnsOf(columns);
952
- if (relations.length === 0)
953
- return /* @__PURE__ */ jsx(Hint, { testId: "database-column-no-relations", children: labels.noRelations });
954
- const numbers = targetColumnsOf(draft.relation, columns, databases, databaseId).filter(
955
- (c) => c.type === "number"
956
- );
957
- return /* @__PURE__ */ jsxs(Fragment, { children: [
958
- /* @__PURE__ */ jsx(Field, { label: labels.relation, children: /* @__PURE__ */ jsx(
959
- ColumnSelect,
960
- {
961
- value: draft.relation,
962
- onChange: (relation) => onChange({ ...draft, relation, column: "" }),
963
- columns: relations,
964
- placeholder: labels.pickRelation,
965
- testId: "database-column-rollup-relation"
966
- }
967
- ) }),
968
- draft.relation && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[8rem_1fr] gap-2", children: [
969
- /* @__PURE__ */ jsx(Field, { label: labels.rollupOp, children: /* @__PURE__ */ jsxs(
970
- Select,
971
- {
972
- value: draft.op,
973
- onValueChange: (op) => onChange({ ...draft, op }),
974
- children: [
975
- /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": "database-column-rollup-op", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
976
- /* @__PURE__ */ jsx(SelectContent, { children: DATABASE_ROLLUP_OPS.map((op) => /* @__PURE__ */ jsx(SelectItem, { value: op, "data-testid": `database-column-rollup-op-${op}`, children: labels.ops[op] }, op)) })
977
- ]
978
- }
979
- ) }),
980
- draft.op !== "count" && /* @__PURE__ */ jsx(Field, { label: labels.rollupColumn, children: numbers.length ? /* @__PURE__ */ jsx(
981
- ColumnSelect,
982
- {
983
- value: draft.column,
984
- onChange: (column) => onChange({ ...draft, column }),
985
- columns: numbers,
986
- placeholder: labels.pickColumn,
987
- testId: "database-column-rollup-column"
988
- }
989
- ) : /* @__PURE__ */ jsx(Hint, { children: labels.noNumberColumns }) })
990
- ] })
991
- ] });
992
- }
993
- var DEFAULT_DATABASE_ROW_DETAIL_LABELS = {
994
- untitled: "Untitled",
995
- fields: "Fields",
996
- documents: "Linked documents",
997
- noDocuments: "No document is linked to this row.",
998
- empty: "Empty",
999
- deleteRow: "Delete row",
1000
- close: "Close",
1001
- computed: "Computed from a relation"
1002
- };
1003
- function DraftInput({
1004
- value,
1005
- type,
1006
- onCommit,
1007
- testId,
1008
- label
1009
- }) {
1010
- const [draft, setDraft] = useState(value);
1011
- useEffect(() => setDraft(value), [value]);
1012
- return /* @__PURE__ */ jsx(
1013
- Input,
1014
- {
1015
- type,
1016
- value: draft,
1017
- "aria-label": label,
1018
- onChange: (e) => setDraft(e.target.value),
1019
- onBlur: () => draft !== value && onCommit(draft),
1020
- onKeyDown: (e) => {
1021
- if (e.key === "Enter") e.currentTarget.blur();
1022
- if (e.key === "Escape") {
1023
- setDraft(value);
1024
- requestAnimationFrame(() => e.target.blur());
1025
- }
1026
- },
1027
- className: "h-8",
1028
- "data-testid": testId
1029
- }
1030
- );
1031
- }
1032
- function PopoverField({
1033
- display,
1034
- emptyLabel,
1035
- testId,
1036
- label,
1037
- renderEditor,
1038
- onCommit
1039
- }) {
1040
- const [open, setOpen] = useState(false);
1041
- const pending = useRef(null);
1042
- const cancelled = useRef(false);
1043
- const close = (keep) => {
1044
- setOpen(false);
1045
- const change = pending.current;
1046
- pending.current = null;
1047
- if (change && !cancelled.current) onCommit(change.value);
1048
- cancelled.current = false;
1049
- };
1050
- return /* @__PURE__ */ jsxs(
1051
- Popover,
1052
- {
1053
- open,
1054
- onOpenChange: (next) => {
1055
- if (next) {
1056
- pending.current = null;
1057
- cancelled.current = false;
1058
- setOpen(true);
1059
- } else close();
1060
- },
1061
- children: [
1062
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1063
- "div",
1064
- {
1065
- role: "button",
1066
- tabIndex: 0,
1067
- "aria-label": label,
1068
- "aria-haspopup": "dialog",
1069
- onKeyDown: (e) => {
1070
- if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) {
1071
- e.preventDefault();
1072
- setOpen(true);
1073
- }
1074
- },
1075
- className: "flex min-h-8 w-full min-w-0 cursor-pointer items-center rounded-md border border-input bg-transparent px-2 py-1 text-left text-sm outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring",
1076
- "data-testid": testId,
1077
- children: display ?? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: emptyLabel })
1078
- }
1079
- ) }),
1080
- /* @__PURE__ */ jsx(
1081
- PopoverContent,
1082
- {
1083
- align: "start",
1084
- className: "w-auto border-0 bg-transparent p-0 shadow-none",
1085
- onEscapeKeyDown: () => {
1086
- cancelled.current = true;
1087
- },
1088
- children: renderEditor({
1089
- onValueChange: (value) => {
1090
- pending.current = { value };
1091
- },
1092
- stopEditing: () => close()
1093
- })
1094
- }
1095
- )
1096
- ]
1097
- }
1098
- );
1099
- }
1100
- function DatabaseRowDetail({
1101
- row,
1102
- columns,
1103
- onChange,
1104
- sources,
1105
- onDelete,
1106
- onClose,
1107
- readOnly = false,
1108
- locale,
1109
- labels,
1110
- className,
1111
- testId = "database-row-detail"
1112
- }) {
1113
- const l = { ...DEFAULT_DATABASE_ROW_DETAIL_LABELS, ...labels };
1114
- const title = row ? rowTitle(row, columns, locale) || l.untitled : "";
1115
- const commit = (column, value) => {
1116
- if (!row || !onChange) return;
1117
- const next = toApiValue(value, column.type);
1118
- if (sameCellValue(next, row.values[column.id])) return;
1119
- onChange(row, column.id, next);
1120
- };
1121
- const fieldEditor = (column) => {
1122
- if (!row) return null;
1123
- const value = row.values[column.id];
1124
- const id = `${testId}-input-${column.id}`;
1125
- const editable = !readOnly && Boolean(onChange) && canEditColumn(column, sources);
1126
- const display = () => {
1127
- switch (column.type) {
1128
- case "select":
1129
- return typeof value === "string" && value ? /* @__PURE__ */ jsx(SelectChip, { value }) : null;
1130
- case "person":
1131
- return cellIds(value).length ? /* @__PURE__ */ jsx(PersonCell, { value, titles: row.titles }) : null;
1132
- case "document":
1133
- case "relation":
1134
- return cellIds(value).length ? /* @__PURE__ */ jsx(
1135
- LinkChipsCell,
1136
- {
1137
- value,
1138
- titles: row.titles,
1139
- kind: column.type,
1140
- onOpenDocument: sources?.onOpenDocument,
1141
- wrap: true
1142
- }
1143
- ) : null;
1144
- case "lookup":
1145
- return /* @__PURE__ */ jsx(LookupCell, { value, titles: row.titles, locale, wrap: true });
1146
- default: {
1147
- const text = formatCell(value, column.type, locale, row.titles);
1148
- return text ? /* @__PURE__ */ jsx("span", { className: "break-words", children: text }) : null;
1149
- }
1150
- }
1151
- };
1152
- if (!editable) {
1153
- const shown = display();
1154
- return /* @__PURE__ */ jsx(
1155
- "div",
1156
- {
1157
- className: cn(
1158
- "flex min-h-8 min-w-0 items-center py-1 text-sm",
1159
- isComputedColumn(column) && "text-muted-foreground"
1160
- ),
1161
- title: isComputedColumn(column) ? l.computed : void 0,
1162
- "data-testid": id,
1163
- children: shown ?? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" })
1164
- }
1165
- );
1166
- }
1167
- switch (column.type) {
1168
- case "checkbox":
1169
- return /* @__PURE__ */ jsx("div", { className: "flex min-h-8 items-center", children: /* @__PURE__ */ jsx(
1170
- Checkbox,
1171
- {
1172
- checked: value === true,
1173
- "aria-label": column.name,
1174
- onCheckedChange: (checked) => commit(column, checked === true),
1175
- "data-testid": id
1176
- }
1177
- ) });
1178
- case "number":
1179
- case "date":
1180
- case "text": {
1181
- const text = value == null ? "" : column.type === "date" ? String(value).slice(0, 10) : String(value);
1182
- return /* @__PURE__ */ jsx(
1183
- DraftInput,
1184
- {
1185
- value: text,
1186
- type: column.type,
1187
- label: column.name,
1188
- onCommit: (next) => commit(column, next),
1189
- testId: id
1190
- }
1191
- );
1192
- }
1193
- case "select":
1194
- return /* @__PURE__ */ jsx(
1195
- PopoverField,
1196
- {
1197
- display: display(),
1198
- emptyLabel: l.empty,
1199
- label: column.name,
1200
- testId: id,
1201
- onCommit: (next) => commit(column, next),
1202
- renderEditor: (editor) => /* @__PURE__ */ jsx(SelectCellEditor, { ...editor, value, options: column.options ?? [] })
1203
- }
1204
- );
1205
- case "person":
1206
- case "document":
1207
- case "relation": {
1208
- const target = column.relation?.database_id;
1209
- const searchRows = sources?.searchRows;
1210
- return /* @__PURE__ */ jsx(
1211
- PopoverField,
1212
- {
1213
- display: display(),
1214
- emptyLabel: l.empty,
1215
- label: column.name,
1216
- testId: id,
1217
- onCommit: (next) => commit(column, next),
1218
- renderEditor: (editor) => /* @__PURE__ */ jsx(
1219
- PickCellEditor,
1220
- {
1221
- ...editor,
1222
- value,
1223
- data: row,
1224
- kind: column.type,
1225
- options: column.type === "person" ? sources?.members : column.type === "document" ? sources?.documents : void 0,
1226
- search: column.type === "relation" && target && searchRows ? (q) => searchRows(target, q) : void 0
1227
- }
1228
- )
1229
- }
1230
- );
1231
- }
1232
- default:
1233
- return null;
1234
- }
1235
- };
1236
- const documents = /* @__PURE__ */ new Map();
1237
- if (row) {
1238
- for (const column of columns) {
1239
- if (column.type !== "document") continue;
1240
- for (const id of cellIds(row.values[column.id])) {
1241
- documents.set(id, [...documents.get(id) ?? [], column.name]);
1242
- }
1243
- }
1244
- }
1245
- const openDocument = sources?.onOpenDocument;
1246
- return /* @__PURE__ */ jsxs(
1247
- DetailPanel,
1248
- {
1249
- title,
1250
- icon: /* @__PURE__ */ jsx(Rows3, { className: "size-4" }),
1251
- isLoading: !row,
1252
- onClose,
1253
- labels: { close: l.close },
1254
- className,
1255
- testId,
1256
- actions: row && onDelete && !readOnly ? /* @__PURE__ */ jsx(
1257
- Button,
1258
- {
1259
- variant: "ghost",
1260
- size: "icon",
1261
- className: "size-8 text-red-text",
1262
- onClick: () => onDelete(row),
1263
- "aria-label": l.deleteRow,
1264
- title: l.deleteRow,
1265
- "data-testid": `${testId}-delete`,
1266
- children: /* @__PURE__ */ jsx(Trash2, { className: "size-4" })
1267
- }
1268
- ) : void 0,
1269
- children: [
1270
- /* @__PURE__ */ jsx(DetailSection, { title: l.fields, testId: `${testId}-fields`, children: /* @__PURE__ */ jsx(DetailFields, { children: columns.map((column) => {
1271
- const Icon = COLUMN_TYPE_ICONS[column.type];
1272
- return /* @__PURE__ */ jsx(
1273
- DetailField,
1274
- {
1275
- label: column.name,
1276
- icon: Icon ? /* @__PURE__ */ jsx(Icon, { className: "size-3.5" }) : void 0,
1277
- testId: `${testId}-field-${column.id}`,
1278
- children: fieldEditor(column)
1279
- },
1280
- column.id
1281
- );
1282
- }) }) }),
1283
- columns.some((c) => c.type === "document") && /* @__PURE__ */ jsx(DetailSection, { title: l.documents, count: documents.size, testId: `${testId}-documents`, children: documents.size ? /* @__PURE__ */ jsx(DetailLinkList, { children: [...documents].map(([id, via]) => /* @__PURE__ */ jsx(
1284
- DetailLinkItem,
1285
- {
1286
- icon: /* @__PURE__ */ jsx(FileText, { className: "size-4" }),
1287
- onClick: openDocument ? () => openDocument(id) : void 0,
1288
- testId: `${testId}-doc-${id}`,
1289
- children: /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
1290
- /* @__PURE__ */ jsx("span", { className: "block truncate font-medium", children: titleOf(id, row?.titles) }),
1291
- /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: via.join(", ") })
1292
- ] })
1293
- },
1294
- id
1295
- )) }) : /* @__PURE__ */ jsx(DetailEmpty, { children: l.noDocuments }) })
1296
- ]
1297
- }
1298
- );
1299
- }
1300
- var escapeHtml = (text) => text.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
1301
- var DEFAULT_DATABASE_TABLE_LABELS = {
1302
- newRow: "New row",
1303
- emptyTitle: "No rows yet",
1304
- emptyMessage: "Add a row, or import a spreadsheet.",
1305
- noColumnsTitle: "No columns yet",
1306
- noColumnsMessage: "Add a column to start.",
1307
- noMatches: "No rows match your search."
1308
- };
1309
- function DatabaseTable({
1310
- columns,
1311
- rows,
1312
- onCellChange,
1313
- onAddRow,
1314
- onAddColumn,
1315
- onEditColumn,
1316
- onOpenRow,
1317
- sources,
1318
- onSelectionChange,
1319
- selected,
1320
- readOnly = false,
1321
- isFiltered = false,
1322
- isLoading,
1323
- locale,
1324
- gridId,
1325
- height = "100%",
1326
- labels,
1327
- columnLabels,
1328
- gridProps,
1329
- testId
1330
- }) {
1331
- const l = { ...DEFAULT_DATABASE_TABLE_LABELS, ...labels };
1332
- const columnDefs = useMemo(
1333
- () => createDatabaseColumns(columns, {
1334
- readOnly,
1335
- onEditColumn,
1336
- onAddColumn,
1337
- onOpenRow,
1338
- sources,
1339
- locale,
1340
- labels: columnLabels
1341
- }),
1342
- [columns, readOnly, onEditColumn, onAddColumn, onOpenRow, sources, locale, columnLabels]
1343
- );
1344
- const noColumns = columns.length === 0;
1345
- const emptyState = {
1346
- icon: /* @__PURE__ */ jsx(Table2, { className: "size-8 text-muted-foreground" }),
1347
- title: l.noColumnsTitle,
1348
- message: readOnly ? void 0 : l.noColumnsMessage,
1349
- action: !readOnly && onAddColumn ? /* @__PURE__ */ jsxs(Button, { onClick: onAddColumn, "data-testid": "database-empty-add-column", className: "gap-1.5", children: [
1350
- /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
1351
- columnLabels?.addColumn ?? DEFAULT_DATABASE_COLUMN_LABELS.addColumn
1352
- ] }) : void 0
1353
- };
1354
- const noRowsText = isFiltered ? l.noMatches : readOnly ? l.emptyTitle : `${l.emptyTitle}. ${l.emptyMessage}`;
1355
- const api = useRef(null);
1356
- const knownColumns = useRef(null);
1357
- useEffect(() => {
1358
- const ids = new Set(columns.map((c) => c.id));
1359
- const added = knownColumns.current ? columns.filter((c) => !knownColumns.current.has(c.id)) : [];
1360
- knownColumns.current = ids;
1361
- const last = added[added.length - 1];
1362
- if (last && api.current) {
1363
- const grid = api.current;
1364
- setTimeout(() => grid.ensureColumnVisible(last.id, "end"), 0);
1365
- }
1366
- }, [columns]);
1367
- const handleCellValueChanged = (event) => {
1368
- const columnId = event.column.getColId();
1369
- if (!event.data || !onCellChange) return;
1370
- onCellChange(event.data, columnId, event.data.values[columnId] ?? null);
1371
- };
1372
- return /* @__PURE__ */ jsxs(
1373
- "div",
1374
- {
1375
- className: "flex h-full min-h-0 flex-col",
1376
- "data-testid": testIdOr(testId, { testId: "database-table" }),
1377
- children: [
1378
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
1379
- GridView,
1380
- {
1381
- testId: "database-grid",
1382
- items: rows,
1383
- columnDefs,
1384
- getItemId: (row) => row.id,
1385
- getRowTestId: (row) => `database-row-${row.id}`,
1386
- getCellTestId: (colId, row) => `database-cell-${row.id}-${colId}`,
1387
- isLoading,
1388
- emptyState,
1389
- showEmpty: noColumns,
1390
- gridId,
1391
- height,
1392
- rowHeight: 38,
1393
- selection: onSelectionChange ? { mode: "multiple", selected: selected ?? [], onSelectionChange, controlled: true } : void 0,
1394
- onCellValueChanged: handleCellValueChanged,
1395
- ...gridProps,
1396
- onGridReady: (params) => {
1397
- api.current = params.api;
1398
- gridProps?.onGridReady?.(params);
1399
- },
1400
- gridOptions: {
1401
- overlayNoRowsTemplate: `<span class="text-sm text-muted-foreground">${escapeHtml(noRowsText)}</span>`,
1402
- singleClickEdit: false,
1403
- stopEditingWhenCellsLoseFocus: true,
1404
- enterNavigatesVerticallyAfterEdit: true,
1405
- undoRedoCellEditing: false,
1406
- ...gridProps?.gridOptions
1407
- }
1408
- }
1409
- ) }),
1410
- !readOnly && onAddRow && columns.length > 0 && /* @__PURE__ */ jsx("div", { className: "border-t border-border px-2 py-1", children: /* @__PURE__ */ jsxs(
1411
- Button,
1412
- {
1413
- variant: "ghost",
1414
- size: "sm",
1415
- onClick: onAddRow,
1416
- "data-testid": "database-add-row",
1417
- className: "gap-1.5 text-muted-foreground",
1418
- children: [
1419
- /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
1420
- l.newRow
1421
- ]
1422
- }
1423
- ) })
1424
- ]
1425
- }
1426
- );
1427
- }
1428
- var DEFAULT_DATABASE_TOOLBAR_LABELS = {
1429
- back: "All databases",
1430
- rename: "Rename database",
1431
- rows: (n) => `${n.toLocaleString()} row${n === 1 ? "" : "s"}`,
1432
- showing: (shown, total) => `Showing ${shown.toLocaleString()} of ${total.toLocaleString()}`,
1433
- search: "Search",
1434
- deleteSelected: (n) => `Delete ${n} row${n === 1 ? "" : "s"}`,
1435
- more: "More actions",
1436
- importRows: "Import rows from a spreadsheet\u2026",
1437
- exportXlsx: "Export to Excel (.xlsx)",
1438
- exportCsv: "Export to CSV",
1439
- deleteDatabase: "Delete database",
1440
- readOnly: "Read-only here: this database belongs to another workspace."
1441
- };
1442
- var SPREADSHEET_ACCEPT = ".csv,.xlsx,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
1443
- function DatabaseToolbar({
1444
- name,
1445
- description,
1446
- total,
1447
- shown,
1448
- search,
1449
- onSearchChange,
1450
- onBack,
1451
- onRename,
1452
- selectedCount = 0,
1453
- onDeleteSelected,
1454
- onImportRows,
1455
- onExport,
1456
- onDeleteDatabase,
1457
- readOnly = false,
1458
- views,
1459
- labels
1460
- }) {
1461
- const l = { ...DEFAULT_DATABASE_TOOLBAR_LABELS, ...labels };
1462
- const [title, setTitle] = useState(name);
1463
- const fileInput = useRef(null);
1464
- useEffect(() => setTitle(name), [name]);
1465
- const commitTitle = () => {
1466
- const next = title.trim();
1467
- if (next && next !== name) onRename?.(next);
1468
- else setTitle(name);
1469
- };
1470
- const onTitleKey = (e) => {
1471
- if (e.key === "Enter") e.currentTarget.blur();
1472
- if (e.key === "Escape") {
1473
- setTitle(name);
1474
- e.currentTarget.blur();
1475
- }
1476
- };
1477
- const onFile = (e) => {
1478
- const file = e.target.files?.[0];
1479
- if (file) onImportRows?.(file);
1480
- e.target.value = "";
1481
- };
1482
- const canRename = !readOnly && Boolean(onRename);
1483
- const hasMenu = Boolean(onExport || !readOnly && (onImportRows || onDeleteDatabase));
1484
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-2 pb-2 pt-1", "data-testid": "database-toolbar", children: [
1485
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
1486
- onBack && /* @__PURE__ */ jsx(
1487
- Button,
1488
- {
1489
- variant: "ghost",
1490
- size: "icon",
1491
- onClick: onBack,
1492
- "aria-label": l.back,
1493
- title: l.back,
1494
- "data-testid": "database-back",
1495
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "size-4" })
1496
- }
1497
- ),
1498
- /* @__PURE__ */ jsx(FileSpreadsheet, { className: "size-5 shrink-0 text-muted-foreground", "aria-hidden": true }),
1499
- canRename ? /* @__PURE__ */ jsx(
1500
- "input",
1501
- {
1502
- value: title,
1503
- "aria-label": l.rename,
1504
- maxLength: 200,
1505
- onChange: (e) => setTitle(e.target.value),
1506
- onBlur: commitTitle,
1507
- onKeyDown: onTitleKey,
1508
- "data-testid": "database-title",
1509
- className: "min-w-0 flex-1 truncate rounded-md bg-transparent px-1 text-xl font-semibold outline-none hover:bg-muted/60 focus:bg-muted/60"
1510
- }
1511
- ) : /* @__PURE__ */ jsx(
1512
- "h2",
1513
- {
1514
- className: "min-w-0 flex-1 truncate px-1 text-xl font-semibold",
1515
- "data-testid": "database-title",
1516
- children: name
1517
- }
1518
- ),
1519
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm text-muted-foreground", "data-testid": "database-row-count", children: shown !== void 0 && shown < total ? l.showing(shown, total) : l.rows(total) })
1520
- ] }),
1521
- description && /* @__PURE__ */ jsx("p", { className: "px-1 text-sm text-muted-foreground", children: description }),
1522
- readOnly && /* @__PURE__ */ jsxs(
1523
- "p",
1524
- {
1525
- className: "flex items-center gap-1.5 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground",
1526
- "data-testid": "database-read-only",
1527
- children: [
1528
- /* @__PURE__ */ jsx(Eye, { className: "size-3.5" }),
1529
- l.readOnly
1530
- ]
1531
- }
1532
- ),
1533
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1534
- /* @__PURE__ */ jsxs("div", { className: "relative max-w-sm flex-1", children: [
1535
- /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }),
1536
- /* @__PURE__ */ jsx(
1537
- Input,
1538
- {
1539
- value: search,
1540
- onChange: (e) => onSearchChange(e.target.value),
1541
- placeholder: l.search,
1542
- "aria-label": l.search,
1543
- className: "h-8 pl-8",
1544
- "data-testid": "database-search"
1545
- }
1546
- )
1547
- ] }),
1548
- /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1549
- views,
1550
- !readOnly && selectedCount > 0 && onDeleteSelected && /* @__PURE__ */ jsxs(
1551
- Button,
1552
- {
1553
- variant: "outline",
1554
- size: "sm",
1555
- onClick: onDeleteSelected,
1556
- className: "gap-1.5 text-red-text",
1557
- "data-testid": "database-delete-rows",
1558
- children: [
1559
- /* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
1560
- l.deleteSelected(selectedCount)
1561
- ]
1562
- }
1563
- ),
1564
- hasMenu && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1565
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1566
- Button,
1567
- {
1568
- variant: "outline",
1569
- size: "icon",
1570
- "aria-label": l.more,
1571
- title: l.more,
1572
- "data-testid": "database-menu",
1573
- children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
1574
- }
1575
- ) }),
1576
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-64", children: [
1577
- !readOnly && onImportRows && /* @__PURE__ */ jsxs(
1578
- DropdownMenuItem,
1579
- {
1580
- onSelect: () => fileInput.current?.click(),
1581
- "data-testid": "database-import-rows",
1582
- children: [
1583
- /* @__PURE__ */ jsx(Upload, { className: "size-4" }),
1584
- l.importRows
1585
- ]
1586
- }
1587
- ),
1588
- onExport && /* @__PURE__ */ jsxs(Fragment, { children: [
1589
- /* @__PURE__ */ jsxs(
1590
- DropdownMenuItem,
1591
- {
1592
- onSelect: () => onExport("xlsx"),
1593
- "data-testid": "database-export-xlsx",
1594
- children: [
1595
- /* @__PURE__ */ jsx(Download, { className: "size-4" }),
1596
- l.exportXlsx
1597
- ]
1598
- }
1599
- ),
1600
- /* @__PURE__ */ jsxs(
1601
- DropdownMenuItem,
1602
- {
1603
- onSelect: () => onExport("csv"),
1604
- "data-testid": "database-export-csv",
1605
- children: [
1606
- /* @__PURE__ */ jsx(Download, { className: "size-4" }),
1607
- l.exportCsv
1608
- ]
1609
- }
1610
- )
1611
- ] }),
1612
- !readOnly && onDeleteDatabase && /* @__PURE__ */ jsxs(Fragment, { children: [
1613
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1614
- /* @__PURE__ */ jsxs(
1615
- DropdownMenuItem,
1616
- {
1617
- onSelect: onDeleteDatabase,
1618
- className: "text-red-text",
1619
- "data-testid": "database-delete",
1620
- children: [
1621
- /* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
1622
- l.deleteDatabase
1623
- ]
1624
- }
1625
- )
1626
- ] })
1627
- ] })
1628
- ] }),
1629
- /* @__PURE__ */ jsx(
1630
- "input",
1631
- {
1632
- ref: fileInput,
1633
- type: "file",
1634
- accept: SPREADSHEET_ACCEPT,
1635
- className: "hidden",
1636
- onChange: onFile,
1637
- "data-testid": "database-import-rows-input"
1638
- }
1639
- )
1640
- ] })
1641
- ] });
1642
- }
1643
- var DEFAULT_DATABASE_COLUMN_DIALOG_LABELS = {
1644
- addTitle: "Add a column",
1645
- editTitle: "Column settings",
1646
- addDescription: "Pick what the column holds; every cell in it is checked against its type.",
1647
- editDescription: "Rename the column or change its options.",
1648
- name: "Name",
1649
- namePlaceholder: "e.g. Client, Reference, Fee",
1650
- type: "Type",
1651
- typeFixed: "A column\u2019s type can\u2019t be changed once it has been added.",
1652
- options: "Options",
1653
- optionPlaceholder: "Add an option and press Enter",
1654
- addOption: "Add",
1655
- removeOption: (option) => `Remove ${option}`,
1656
- needsOptions: "A select column needs at least one option.",
1657
- cancel: "Cancel",
1658
- add: "Add column",
1659
- save: "Save",
1660
- saving: "Saving\u2026",
1661
- delete: "Delete column",
1662
- confirmDelete: "Delete this column and every value in it?"
1663
- };
1664
- var NO_RELATION = { database_id: "", two_way: false, pair_name: "" };
1665
- var NO_LOOKUP = { relation: "", column: "" };
1666
- var NO_ROLLUP = { relation: "", column: "", op: "count" };
1667
- var NO_COLUMNS = [];
1668
- var NO_DATABASES = [];
1669
- function configComplete(type, editing, relation, lookup, rollup) {
1670
- if (type === "relation") return editing || Boolean(relation.database_id);
1671
- if (type === "lookup") return Boolean(lookup.relation && lookup.column);
1672
- if (type === "rollup") return Boolean(rollup.relation && (rollup.op === "count" || rollup.column));
1673
- return true;
1674
- }
1675
- function DatabaseColumnDialog({
1676
- open,
1677
- onOpenChange,
1678
- column,
1679
- databaseId,
1680
- databaseName,
1681
- columns = NO_COLUMNS,
1682
- databases = NO_DATABASES,
1683
- onSave,
1684
- onDelete,
1685
- isSaving = false,
1686
- error,
1687
- labels,
1688
- typeLabels,
1689
- configLabels
1690
- }) {
1691
- const l = { ...DEFAULT_DATABASE_COLUMN_DIALOG_LABELS, ...labels };
1692
- const tl = { ...DEFAULT_DATABASE_COLUMN_TYPE_LABELS, ...typeLabels };
1693
- const cl = { ...DEFAULT_COLUMN_CONFIG_LABELS, ...configLabels };
1694
- const editing = Boolean(column);
1695
- const [name, setName] = useState("");
1696
- const [type, setType] = useState("text");
1697
- const [options, setOptions] = useState([]);
1698
- const [optionDraft, setOptionDraft] = useState("");
1699
- const [confirmingDelete, setConfirmingDelete] = useState(false);
1700
- const [relation, setRelation] = useState(NO_RELATION);
1701
- const [lookup, setLookup] = useState(NO_LOOKUP);
1702
- const [rollup, setRollup] = useState(NO_ROLLUP);
1703
- useEffect(() => {
1704
- if (!open) return;
1705
- setName(column?.name ?? "");
1706
- setType(column?.type ?? "text");
1707
- setOptions(column?.options ?? []);
1708
- setOptionDraft("");
1709
- setConfirmingDelete(false);
1710
- setRelation(NO_RELATION);
1711
- setLookup(column?.lookup ? { ...column.lookup } : NO_LOOKUP);
1712
- setRollup(
1713
- column?.rollup ? {
1714
- relation: column.rollup.relation,
1715
- column: column.rollup.column ?? "",
1716
- op: column.rollup.op ?? "count"
1717
- } : NO_ROLLUP
1718
- );
1719
- }, [open, column]);
1720
- const addOption = () => {
1721
- const option = optionDraft.trim();
1722
- if (option && !options.some((o) => o.toLowerCase() === option.toLowerCase())) {
1723
- setOptions([...options, option]);
1724
- }
1725
- setOptionDraft("");
1726
- };
1727
- const pendingOptions = optionDraft.trim() ? [...options, optionDraft.trim()] : options;
1728
- const missingOptions = type === "select" && pendingOptions.length === 0;
1729
- const canSave = name.trim().length > 0 && !missingOptions && configComplete(type, editing, relation, lookup, rollup) && !isSaving;
1730
- const submit = (e) => {
1731
- e.preventDefault();
1732
- if (!canSave) return;
1733
- const draft = {
1734
- name: name.trim(),
1735
- type,
1736
- options: type === "select" ? Array.from(new Set(pendingOptions)) : null
1737
- };
1738
- if (type === "relation" && !editing)
1739
- draft.relation = {
1740
- database_id: relation.database_id,
1741
- two_way: relation.two_way,
1742
- pair_name: relation.two_way ? relation.pair_name.trim() || null : null
1743
- };
1744
- if (type === "lookup") draft.lookup = { ...lookup };
1745
- if (type === "rollup")
1746
- draft.rollup = {
1747
- relation: rollup.relation,
1748
- op: rollup.op,
1749
- column: rollup.op === "count" ? null : rollup.column
1750
- };
1751
- onSave(draft);
1752
- };
1753
- const onOptionKey = (e) => {
1754
- if (e.key === "Enter") {
1755
- e.preventDefault();
1756
- addOption();
1757
- }
1758
- };
1759
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsx(DialogContent, { className: "sm:max-w-md", "data-testid": "database-column-dialog", children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "flex flex-col gap-4", children: [
1760
- /* @__PURE__ */ jsxs(DialogHeader, { children: [
1761
- /* @__PURE__ */ jsx(DialogTitle, { children: editing ? l.editTitle : l.addTitle }),
1762
- /* @__PURE__ */ jsx(DialogDescription, { children: editing ? l.editDescription : l.addDescription })
1763
- ] }),
1764
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
1765
- /* @__PURE__ */ jsx(Label, { htmlFor: "database-column-name", children: l.name }),
1766
- /* @__PURE__ */ jsx(
1767
- Input,
1768
- {
1769
- id: "database-column-name",
1770
- autoFocus: true,
1771
- value: name,
1772
- maxLength: 200,
1773
- placeholder: l.namePlaceholder,
1774
- onChange: (e) => setName(e.target.value),
1775
- "data-testid": "database-column-name"
1776
- }
1777
- )
1778
- ] }),
1779
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
1780
- /* @__PURE__ */ jsx(Label, { children: l.type }),
1781
- /* @__PURE__ */ jsxs(
1782
- Select,
1783
- {
1784
- value: type,
1785
- onValueChange: (v) => setType(v),
1786
- disabled: editing,
1787
- children: [
1788
- /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": "database-column-type", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
1789
- /* @__PURE__ */ jsx(SelectContent, { children: DATABASE_COLUMN_TYPES.map((t) => {
1790
- const Icon = COLUMN_TYPE_ICONS[t];
1791
- return /* @__PURE__ */ jsx(SelectItem, { value: t, "data-testid": `database-column-type-${t}`, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
1792
- /* @__PURE__ */ jsx(Icon, { className: "size-4 text-muted-foreground" }),
1793
- tl[t]
1794
- ] }) }, t);
1795
- }) })
1796
- ]
1797
- }
1798
- ),
1799
- editing && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: l.typeFixed })
1800
- ] }),
1801
- type === "select" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
1802
- /* @__PURE__ */ jsx(Label, { htmlFor: "database-column-option", children: l.options }),
1803
- options.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", "data-testid": "database-column-options", children: options.map((option) => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5", children: [
1804
- /* @__PURE__ */ jsx(SelectChip, { value: option }),
1805
- /* @__PURE__ */ jsx(
1806
- "button",
1807
- {
1808
- type: "button",
1809
- "aria-label": l.removeOption(option),
1810
- onClick: () => setOptions(options.filter((o) => o !== option)),
1811
- className: "rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
1812
- children: /* @__PURE__ */ jsx(X, { className: "size-3" })
1813
- }
1814
- )
1815
- ] }, option)) }),
1816
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1817
- /* @__PURE__ */ jsx(
1818
- Input,
1819
- {
1820
- id: "database-column-option",
1821
- value: optionDraft,
1822
- maxLength: 200,
1823
- placeholder: l.optionPlaceholder,
1824
- onChange: (e) => setOptionDraft(e.target.value),
1825
- onKeyDown: onOptionKey,
1826
- "data-testid": "database-column-option-input"
1827
- }
1828
- ),
1829
- /* @__PURE__ */ jsx(
1830
- Button,
1831
- {
1832
- type: "button",
1833
- variant: "outline",
1834
- onClick: addOption,
1835
- disabled: !optionDraft.trim(),
1836
- children: l.addOption
1837
- }
1838
- )
1839
- ] }),
1840
- missingOptions && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: l.needsOptions })
1841
- ] }),
1842
- type === "relation" && /* @__PURE__ */ jsx(
1843
- RelationFields,
1844
- {
1845
- draft: relation,
1846
- onChange: setRelation,
1847
- databases,
1848
- databaseId,
1849
- databaseName,
1850
- editing,
1851
- column,
1852
- labels: cl
1853
- }
1854
- ),
1855
- type === "lookup" && /* @__PURE__ */ jsx(
1856
- LookupFields,
1857
- {
1858
- draft: lookup,
1859
- onChange: setLookup,
1860
- columns,
1861
- databases,
1862
- databaseId,
1863
- labels: cl
1864
- }
1865
- ),
1866
- type === "rollup" && /* @__PURE__ */ jsx(
1867
- RollupFields,
1868
- {
1869
- draft: rollup,
1870
- onChange: setRollup,
1871
- columns,
1872
- databases,
1873
- databaseId,
1874
- labels: cl
1875
- }
1876
- ),
1877
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-red-text", "data-testid": "database-column-error", children: error }),
1878
- /* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row items-center gap-2 sm:justify-between", children: [
1879
- editing && onDelete ? confirmingDelete ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm", children: [
1880
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: l.confirmDelete }),
1881
- /* @__PURE__ */ jsx(
1882
- Button,
1883
- {
1884
- type: "button",
1885
- variant: "destructive",
1886
- size: "sm",
1887
- onClick: onDelete,
1888
- "data-testid": "database-column-delete-confirm",
1889
- children: l.delete
1890
- }
1891
- )
1892
- ] }) : /* @__PURE__ */ jsxs(
1893
- Button,
1894
- {
1895
- type: "button",
1896
- variant: "ghost",
1897
- size: "sm",
1898
- onClick: () => setConfirmingDelete(true),
1899
- className: "gap-1.5 text-red-text",
1900
- "data-testid": "database-column-delete",
1901
- children: [
1902
- /* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
1903
- l.delete
1904
- ]
1905
- }
1906
- ) : /* @__PURE__ */ jsx("span", {}),
1907
- /* @__PURE__ */ jsxs("span", { className: "flex gap-2", children: [
1908
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: l.cancel }),
1909
- /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !canSave, "data-testid": "database-column-save", children: isSaving ? l.saving : editing ? l.save : l.add })
1910
- ] })
1911
- ] })
1912
- ] }) }) });
1913
- }
1914
- var DEFAULT_DATABASE_LIST_LABELS = {
1915
- title: "Databases",
1916
- subtitle: "Tables your team keeps and searches \u2014 a client database, billing records, precedents.",
1917
- newDatabase: "New database",
1918
- importSpreadsheet: "Import spreadsheet",
1919
- importing: "Importing\u2026",
1920
- ownGroup: "In this workspace",
1921
- sharedGroup: "Shared with everyone",
1922
- readOnly: "Read-only",
1923
- rows: (n) => `${n.toLocaleString()} row${n === 1 ? "" : "s"}`,
1924
- columns: (n) => `${n} column${n === 1 ? "" : "s"}`,
1925
- emptyTitle: "No databases yet",
1926
- emptyMessage: "Start from a blank table, or import a spreadsheet (.xlsx or .csv) \u2014 its columns and their types come with it.",
1927
- loading: "Loading databases\u2026"
1928
- };
1929
- var SPREADSHEET_ACCEPT2 = ".csv,.xlsx,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
1930
- function DatabaseCard({
1931
- database,
1932
- onOpen,
1933
- l
1934
- }) {
1935
- return /* @__PURE__ */ jsxs(
1936
- "button",
1937
- {
1938
- type: "button",
1939
- onClick: onOpen,
1940
- "data-testid": `database-card-${database.id}`,
1941
- className: cn(
1942
- "flex min-h-32 flex-col gap-2 rounded-lg border border-border bg-background p-4 text-left transition-colors",
1943
- "hover:border-primary/40 hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1944
- ),
1945
- children: [
1946
- /* @__PURE__ */ jsxs("span", { className: "flex w-full items-start gap-2", children: [
1947
- /* @__PURE__ */ jsx(FileSpreadsheet, { className: "mt-0.5 size-5 shrink-0 text-muted-foreground", "aria-hidden": true }),
1948
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: database.name }),
1949
- !database.editable && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground", children: [
1950
- /* @__PURE__ */ jsx(Eye, { className: "size-3" }),
1951
- l.readOnly
1952
- ] })
1953
- ] }),
1954
- database.description && /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-sm text-muted-foreground", children: database.description }),
1955
- /* @__PURE__ */ jsxs("span", { className: "mt-auto flex w-full items-center gap-3 text-xs text-muted-foreground", children: [
1956
- /* @__PURE__ */ jsx("span", { children: l.rows(database.row_count) }),
1957
- /* @__PURE__ */ jsx("span", { children: l.columns(database.columns.length) }),
1958
- /* @__PURE__ */ jsx("span", { className: "ml-auto flex items-center gap-1", "aria-hidden": true, children: database.columns.slice(0, 6).map((c) => {
1959
- const Icon = COLUMN_TYPE_ICONS[c.type];
1960
- return /* @__PURE__ */ jsx(Icon, { className: "size-3.5" }, c.id);
1961
- }) })
1962
- ] })
1963
- ]
1964
- }
1965
- );
1966
- }
1967
- function DatabaseList({
1968
- databases,
1969
- onOpen,
1970
- onNew,
1971
- onImport,
1972
- isImporting = false,
1973
- isLoading = false,
1974
- labels
1975
- }) {
1976
- const l = { ...DEFAULT_DATABASE_LIST_LABELS, ...labels };
1977
- const fileInput = useRef(null);
1978
- const own = databases.filter((d) => d.editable);
1979
- const shared = databases.filter((d) => !d.editable);
1980
- const onFile = (e) => {
1981
- const file = e.target.files?.[0];
1982
- if (file) onImport?.(file);
1983
- e.target.value = "";
1984
- };
1985
- const actions = /* @__PURE__ */ jsxs("span", { className: "flex gap-2", children: [
1986
- onImport && /* @__PURE__ */ jsxs(
1987
- Button,
1988
- {
1989
- variant: "outline",
1990
- onClick: () => fileInput.current?.click(),
1991
- disabled: isImporting,
1992
- className: "gap-1.5",
1993
- "data-testid": "database-import",
1994
- children: [
1995
- /* @__PURE__ */ jsx(Upload, { className: "size-4" }),
1996
- isImporting ? l.importing : l.importSpreadsheet
1997
- ]
1998
- }
1999
- ),
2000
- onNew && /* @__PURE__ */ jsxs(Button, { onClick: onNew, className: "gap-1.5", "data-testid": "database-new", children: [
2001
- /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
2002
- l.newDatabase
2003
- ] })
2004
- ] });
2005
- const grid = (items) => /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3", children: items.map((d) => /* @__PURE__ */ jsx(DatabaseCard, { database: d, onOpen: () => onOpen(d), l }, d.id)) });
2006
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6 p-2 sm:p-4", "data-testid": "database-list", children: [
2007
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
2008
- /* @__PURE__ */ jsxs("div", { children: [
2009
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: l.title }),
2010
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: l.subtitle })
2011
- ] }),
2012
- actions
2013
- ] }),
2014
- isLoading ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: l.loading }) : databases.length === 0 ? /* @__PURE__ */ jsxs(
2015
- "div",
2016
- {
2017
- className: "flex flex-col items-center gap-2 rounded-lg border border-dashed border-border px-6 py-12 text-center",
2018
- "data-testid": "database-list-empty",
2019
- children: [
2020
- /* @__PURE__ */ jsx(FileSpreadsheet, { className: "size-8 text-muted-foreground" }),
2021
- /* @__PURE__ */ jsx("h3", { className: "text-lg font-medium", children: l.emptyTitle }),
2022
- /* @__PURE__ */ jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: l.emptyMessage })
2023
- ]
2024
- }
2025
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
2026
- own.length > 0 && /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
2027
- shared.length > 0 && /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground", children: l.ownGroup }),
2028
- grid(own)
2029
- ] }),
2030
- shared.length > 0 && /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", "data-testid": "database-list-shared", children: [
2031
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground", children: l.sharedGroup }),
2032
- grid(shared)
2033
- ] })
2034
- ] }),
2035
- /* @__PURE__ */ jsx(
2036
- "input",
2037
- {
2038
- ref: fileInput,
2039
- type: "file",
2040
- accept: SPREADSHEET_ACCEPT2,
2041
- className: "hidden",
2042
- onChange: onFile,
2043
- "data-testid": "database-import-input"
2044
- }
2045
- )
2046
- ] });
2047
- }
2048
- var DEFAULT_NEW_DATABASE_DIALOG_LABELS = {
2049
- title: "New database",
2050
- description: "A table your workspace keeps: add columns of the kinds you need, then rows.",
2051
- name: "Name",
2052
- namePlaceholder: "e.g. Client database",
2053
- about: "Description",
2054
- aboutPlaceholder: "Optional: what it holds and who keeps it",
2055
- cancel: "Cancel",
2056
- create: "Create",
2057
- creating: "Creating\u2026"
2058
- };
2059
- function NewDatabaseDialog({
2060
- open,
2061
- onOpenChange,
2062
- onCreate,
2063
- isCreating = false,
2064
- error,
2065
- labels
2066
- }) {
2067
- const l = { ...DEFAULT_NEW_DATABASE_DIALOG_LABELS, ...labels };
2068
- const [name, setName] = useState("");
2069
- const [about, setAbout] = useState("");
2070
- useEffect(() => {
2071
- if (open) {
2072
- setName("");
2073
- setAbout("");
2074
- }
2075
- }, [open]);
2076
- const submit = (e) => {
2077
- e.preventDefault();
2078
- if (!name.trim() || isCreating) return;
2079
- onCreate({ name: name.trim(), description: about.trim() || null });
2080
- };
2081
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsx(DialogContent, { className: "sm:max-w-md", "data-testid": "new-database-dialog", children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "flex flex-col gap-4", children: [
2082
- /* @__PURE__ */ jsxs(DialogHeader, { children: [
2083
- /* @__PURE__ */ jsx(DialogTitle, { children: l.title }),
2084
- /* @__PURE__ */ jsx(DialogDescription, { children: l.description })
2085
- ] }),
2086
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
2087
- /* @__PURE__ */ jsx(Label, { htmlFor: "new-database-name", children: l.name }),
2088
- /* @__PURE__ */ jsx(
2089
- Input,
2090
- {
2091
- id: "new-database-name",
2092
- autoFocus: true,
2093
- value: name,
2094
- maxLength: 200,
2095
- placeholder: l.namePlaceholder,
2096
- onChange: (e) => setName(e.target.value),
2097
- "data-testid": "new-database-name"
2098
- }
2099
- )
2100
- ] }),
2101
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
2102
- /* @__PURE__ */ jsx(Label, { htmlFor: "new-database-about", children: l.about }),
2103
- /* @__PURE__ */ jsx(
2104
- Textarea,
2105
- {
2106
- id: "new-database-about",
2107
- value: about,
2108
- maxLength: 2e3,
2109
- rows: 2,
2110
- placeholder: l.aboutPlaceholder,
2111
- onChange: (e) => setAbout(e.target.value),
2112
- "data-testid": "new-database-description"
2113
- }
2114
- )
2115
- ] }),
2116
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-red-text", children: error }),
2117
- /* @__PURE__ */ jsxs(DialogFooter, { children: [
2118
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: l.cancel }),
2119
- /* @__PURE__ */ jsx(
2120
- Button,
2121
- {
2122
- type: "submit",
2123
- disabled: !name.trim() || isCreating,
2124
- "data-testid": "new-database-create",
2125
- children: isCreating ? l.creating : l.create
2126
- }
2127
- )
2128
- ] })
2129
- ] }) }) });
2130
- }
2131
-
2132
- export { ADD_COLUMN_COL_ID, COLUMN_TYPE_ICONS, COMPUTED_COLUMN_TYPES, DATABASE_COLUMN_TYPES, DATABASE_ROLLUP_OPS, DEFAULT_COLUMN_CONFIG_LABELS, DEFAULT_DATABASE_COLUMN_DIALOG_LABELS, DEFAULT_DATABASE_COLUMN_LABELS, DEFAULT_DATABASE_COLUMN_TYPE_LABELS, DEFAULT_DATABASE_LIST_LABELS, DEFAULT_DATABASE_ROW_DETAIL_LABELS, DEFAULT_DATABASE_TABLE_LABELS, DEFAULT_DATABASE_TOOLBAR_LABELS, DEFAULT_NEW_DATABASE_DIALOG_LABELS, DEFAULT_PICK_CELL_EDITOR_LABELS, DatabaseColumnDialog, DatabaseList, DatabaseRowDetail, DatabaseTable, DatabaseToolbar, LINK_COLUMN_TYPES, LinkChip, LinkChipsCell, LookupCell, NewDatabaseDialog, OPEN_ROW_COL_ID, PersonCell, PersonChip, PickCellEditor, SelectCellEditor, SelectChip, canEditColumn, cellIds, cellList, columnWidth, compareCells, createDatabaseColumns, formatCell, formatDateValue, formatLookupValue, formatNumberValue, isComputedColumn, optionTone, rowTitle, sameCellValue, titleOf, toApiValue };
2133
8
  //# sourceMappingURL=index.js.map
2134
9
  //# sourceMappingURL=index.js.map