@arbidocs/blocks 0.4.77 → 0.4.79

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 (85) hide show
  1. package/dist/agents/index.cjs +287 -275
  2. package/dist/agents/index.js +3 -3
  3. package/dist/agents/tasks/WorkflowThreadStrip.d.ts +9 -4
  4. package/dist/agents/workflows/StepBarFrame.d.ts +75 -0
  5. package/dist/agents/workflows/WorkflowRunner.d.ts +5 -1
  6. package/dist/agents/workflows/WorkflowStepBar.d.ts +9 -11
  7. package/dist/agents/workflows/index.d.ts +2 -0
  8. package/dist/agents/workflows/stepWindow.d.ts +59 -0
  9. package/dist/chat/ChatPage.d.ts +1 -1
  10. package/dist/chat/chat-page-types.d.ts +5 -0
  11. package/dist/chat/index.cjs +124 -124
  12. package/dist/chat/index.js +2 -2
  13. package/dist/{chunk-JFUN4D7D.cjs → chunk-3YET6D5E.cjs} +150 -146
  14. package/dist/chunk-3YET6D5E.cjs.map +1 -0
  15. package/dist/{chunk-B3WCKBJ7.cjs → chunk-5XVWGJ2H.cjs} +3 -3
  16. package/dist/{chunk-B3WCKBJ7.cjs.map → chunk-5XVWGJ2H.cjs.map} +1 -1
  17. package/dist/{chunk-34UW6XUQ.js → chunk-6GXF62XN.js} +566 -171
  18. package/dist/chunk-6GXF62XN.js.map +1 -0
  19. package/dist/{chunk-HEVVT6IT.js → chunk-BMOL5DN6.js} +126 -122
  20. package/dist/chunk-BMOL5DN6.js.map +1 -0
  21. package/dist/chunk-CWAZNXC5.js +441 -0
  22. package/dist/chunk-CWAZNXC5.js.map +1 -0
  23. package/dist/{chunk-NIP5BOP7.js → chunk-EWSEQEPL.js} +5 -4
  24. package/dist/chunk-EWSEQEPL.js.map +1 -0
  25. package/dist/{chunk-UEU23PTW.js → chunk-ICBAFX5B.js} +3 -3
  26. package/dist/{chunk-UEU23PTW.js.map → chunk-ICBAFX5B.js.map} +1 -1
  27. package/dist/{chunk-EKIUMNCN.cjs → chunk-IF6VAYIA.cjs} +5 -5
  28. package/dist/{chunk-EKIUMNCN.cjs.map → chunk-IF6VAYIA.cjs.map} +1 -1
  29. package/dist/chunk-MLAHDAN5.cjs +450 -0
  30. package/dist/chunk-MLAHDAN5.cjs.map +1 -0
  31. package/dist/{chunk-HOD6OM7L.cjs → chunk-MP6FK43E.cjs} +5 -4
  32. package/dist/chunk-MP6FK43E.cjs.map +1 -0
  33. package/dist/{chunk-GJV2G4FC.js → chunk-O35LRB3P.js} +3 -3
  34. package/dist/{chunk-GJV2G4FC.js.map → chunk-O35LRB3P.js.map} +1 -1
  35. package/dist/{chunk-GTXOV7IJ.cjs → chunk-XMBYDLZQ.cjs} +4 -4
  36. package/dist/{chunk-GTXOV7IJ.cjs.map → chunk-XMBYDLZQ.cjs.map} +1 -1
  37. package/dist/{chunk-OEM5ARVM.js → chunk-XQDM6T6C.js} +3 -3
  38. package/dist/{chunk-OEM5ARVM.js.map → chunk-XQDM6T6C.js.map} +1 -1
  39. package/dist/{chunk-3XR3IEGU.cjs → chunk-YGUKTFKW.cjs} +572 -167
  40. package/dist/chunk-YGUKTFKW.cjs.map +1 -0
  41. package/dist/connect/index.cjs +233 -77
  42. package/dist/connect/index.cjs.map +1 -1
  43. package/dist/connect/index.d.ts +2 -0
  44. package/dist/connect/index.js +191 -37
  45. package/dist/connect/index.js.map +1 -1
  46. package/dist/connection/ConnectedTagMatrix.d.ts +20 -0
  47. package/dist/connection/ConnectedWorkflowRunner.d.ts +13 -1
  48. package/dist/connection/WorkflowRunThread.d.ts +21 -0
  49. package/dist/databases/ColumnConfigFields.d.ts +68 -0
  50. package/dist/databases/DatabaseColumnDialog.d.ts +27 -2
  51. package/dist/databases/DatabaseTable.d.ts +8 -1
  52. package/dist/databases/PickCellEditor.d.ts +21 -0
  53. package/dist/databases/column-types.d.ts +73 -9
  54. package/dist/databases/databaseColumns.d.ts +4 -0
  55. package/dist/databases/index.cjs +758 -27
  56. package/dist/databases/index.cjs.map +1 -1
  57. package/dist/databases/index.d.ts +4 -1
  58. package/dist/databases/index.js +745 -31
  59. package/dist/databases/index.js.map +1 -1
  60. package/dist/databases/linkCells.d.ts +48 -0
  61. package/dist/files/index.cjs +2 -2
  62. package/dist/files/index.js +2 -2
  63. package/dist/index.cjs +624 -579
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +9 -8
  67. package/dist/index.js.map +1 -1
  68. package/dist/settings/index.cjs +35 -35
  69. package/dist/settings/index.js +3 -3
  70. package/dist/studio/index.cjs +3 -3
  71. package/dist/studio/index.js +2 -2
  72. package/dist/tags/TagMatrix.d.ts +64 -0
  73. package/dist/tags/index.d.ts +3 -0
  74. package/dist/tags/tagMatrixModel.d.ts +39 -0
  75. package/dist/workflow-canvas/index.cjs +46 -46
  76. package/dist/workflow-canvas/index.js +3 -3
  77. package/dist/workspaces/index.cjs +1 -1
  78. package/dist/workspaces/index.js +1 -1
  79. package/package.json +4 -4
  80. package/dist/chunk-34UW6XUQ.js.map +0 -1
  81. package/dist/chunk-3XR3IEGU.cjs.map +0 -1
  82. package/dist/chunk-HEVVT6IT.js.map +0 -1
  83. package/dist/chunk-HOD6OM7L.cjs.map +0 -1
  84. package/dist/chunk-JFUN4D7D.cjs.map +0 -1
  85. package/dist/chunk-NIP5BOP7.js.map +0 -1
@@ -3,9 +3,9 @@ import { GridView } from '../chunk-ZPLG7QYF.js';
3
3
  import '../chunk-SLSBJRWW.js';
4
4
  import { testIdOr } from '../chunk-7F6CBP3Z.js';
5
5
  import '../chunk-VUNV25KB.js';
6
- import { ChevronDown, CheckSquare, CalendarDays, Hash, Type, Check, Plus, ArrowLeft, FileSpreadsheet, Eye, Search, Trash2, MoreHorizontal, Upload, Download, X, Table2 } from 'lucide-react';
7
- import { cn, Button, Input, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DialogFooter, Textarea } from '@arbidocs/react/ui';
8
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+ import { Sigma, Search, ArrowUpRight, FileText, UserRound, ChevronDown, CheckSquare, CalendarDays, Hash, Type, X, Loader2, Check, Plus, ArrowLeft, FileSpreadsheet, Eye, Trash2, MoreHorizontal, Upload, Download, Table2, Lock } from 'lucide-react';
7
+ import { cn, Button, Input, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DialogFooter, Textarea, Switch } from '@arbidocs/react/ui';
8
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
9
  import { useState, useRef, useEffect, useMemo } from 'react';
10
10
 
11
11
  // src/databases/column-types.ts
@@ -14,14 +14,34 @@ var DATABASE_COLUMN_TYPES = [
14
14
  "number",
15
15
  "date",
16
16
  "checkbox",
17
- "select"
17
+ "select",
18
+ "person",
19
+ "document",
20
+ "relation",
21
+ "lookup",
22
+ "rollup"
18
23
  ];
24
+ var COMPUTED_COLUMN_TYPES = /* @__PURE__ */ new Set([
25
+ "lookup",
26
+ "rollup"
27
+ ]);
28
+ var LINK_COLUMN_TYPES = /* @__PURE__ */ new Set([
29
+ "document",
30
+ "relation"
31
+ ]);
32
+ var isComputedColumn = (column) => COMPUTED_COLUMN_TYPES.has(column.type);
33
+ var DATABASE_ROLLUP_OPS = ["count", "sum", "avg", "min", "max"];
19
34
  var DEFAULT_DATABASE_COLUMN_TYPE_LABELS = {
20
35
  text: "Text",
21
36
  number: "Number",
22
37
  date: "Date",
23
38
  checkbox: "Checkbox",
24
- select: "Select"
39
+ select: "Select",
40
+ person: "Person",
41
+ document: "Documents",
42
+ relation: "Relation",
43
+ lookup: "Lookup",
44
+ rollup: "Rollup"
25
45
  };
26
46
  var OPTION_TONES = [
27
47
  "bg-blue-bg text-blue-text",
@@ -36,7 +56,25 @@ function optionTone(option) {
36
56
  for (const ch of option.toLowerCase()) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
37
57
  return OPTION_TONES[hash % OPTION_TONES.length];
38
58
  }
39
- var isEmpty = (value) => value === null || value === void 0 || typeof value === "string" && value.trim() === "";
59
+ var isEmpty = (value) => value === null || value === void 0 || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
60
+ function cellList(value) {
61
+ if (isEmpty(value)) return [];
62
+ return Array.isArray(value) ? value.filter((v) => !isEmpty(v)) : [value];
63
+ }
64
+ function cellIds(value) {
65
+ return cellList(value).filter((v) => typeof v === "string");
66
+ }
67
+ function titleOf(id, titles) {
68
+ return titles?.[id] ?? id;
69
+ }
70
+ function sameCellValue(a, b) {
71
+ if (Array.isArray(a) || Array.isArray(b)) {
72
+ const la = cellList(a);
73
+ const lb = cellList(b);
74
+ return la.length === lb.length && la.every((v, i) => v === lb[i]);
75
+ }
76
+ return (a ?? null) === (b ?? null);
77
+ }
40
78
  function formatDateValue(value, locale) {
41
79
  const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
42
80
  if (day) {
@@ -49,15 +87,37 @@ function formatDateValue(value, locale) {
49
87
  function formatNumberValue(value, locale) {
50
88
  return value.toLocaleString(locale, { maximumFractionDigits: 6 });
51
89
  }
52
- function formatCell(value, type, locale) {
90
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
91
+ function formatLookupValue(value, titles, locale) {
53
92
  if (isEmpty(value)) return "";
54
- if (type === "number" && typeof value === "number") return formatNumberValue(value, locale);
93
+ if (typeof value === "number") return formatNumberValue(value, locale);
94
+ if (typeof value === "boolean") return value ? "Yes" : "No";
95
+ if (typeof value === "string") {
96
+ if (titles?.[value] !== void 0) return titles[value];
97
+ if (ISO_DATE.test(value)) return formatDateValue(value, locale);
98
+ }
99
+ return String(value);
100
+ }
101
+ var NAMED_TYPES = /* @__PURE__ */ new Set([
102
+ "person",
103
+ "document",
104
+ "relation"
105
+ ]);
106
+ function formatCell(value, type, locale, titles) {
107
+ if (isEmpty(value)) return "";
108
+ if ((type === "number" || type === "rollup") && typeof value === "number")
109
+ return formatNumberValue(value, locale);
55
110
  if (type === "date" && typeof value === "string") return formatDateValue(value, locale);
56
111
  if (type === "checkbox") return value ? "Yes" : "No";
112
+ if (NAMED_TYPES.has(type))
113
+ return cellIds(value).map((id) => titleOf(id, titles)).join(", ");
114
+ if (type === "lookup")
115
+ return cellList(value).map((v) => formatLookupValue(v, titles, locale)).join(", ");
57
116
  return String(value);
58
117
  }
59
118
  function toApiValue(value, type) {
60
119
  if (isEmpty(value)) return null;
120
+ if (LINK_COLUMN_TYPES.has(type)) return cellIds(value);
61
121
  if (type === "checkbox") return Boolean(value);
62
122
  if (type === "date" && value instanceof Date) {
63
123
  const pad = (n) => String(n).padStart(2, "0");
@@ -69,12 +129,299 @@ function toApiValue(value, type) {
69
129
  }
70
130
  return value;
71
131
  }
72
- function compareCells(a, b, type) {
132
+ function compareCells(a, b, type, titlesA, titlesB) {
73
133
  const ea = isEmpty(a);
74
134
  const eb = isEmpty(b);
75
135
  if (ea || eb) return ea === eb ? 0 : ea ? 1 : -1;
76
- if (type === "number" || type === "checkbox") return Number(a) - Number(b);
77
- return String(a).localeCompare(String(b), void 0, { sensitivity: "base", numeric: true });
136
+ if (type === "number" || type === "checkbox" || type === "rollup") return Number(a) - Number(b);
137
+ const shown = NAMED_TYPES.has(type) || type === "lookup";
138
+ const ta = shown ? formatCell(a, type, void 0, titlesA) : String(a);
139
+ const tb = shown ? formatCell(b, type, void 0, titlesB) : String(b);
140
+ return ta.localeCompare(tb, void 0, { sensitivity: "base", numeric: true });
141
+ }
142
+ 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";
143
+ function initials(name) {
144
+ const parts = name.trim().split(/\s+/).filter(Boolean);
145
+ const letters = parts.length > 1 ? parts[0][0] + parts[parts.length - 1][0] : name.slice(0, 2);
146
+ return letters.toUpperCase();
147
+ }
148
+ function PersonChip({ name }) {
149
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5 text-sm", children: [
150
+ /* @__PURE__ */ jsx(
151
+ "span",
152
+ {
153
+ "aria-hidden": true,
154
+ className: "flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-muted-foreground",
155
+ children: initials(name)
156
+ }
157
+ ),
158
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: name })
159
+ ] });
160
+ }
161
+ function LinkChip({
162
+ label,
163
+ kind,
164
+ onOpen,
165
+ testId
166
+ }) {
167
+ const Icon = kind === "document" ? FileText : ArrowUpRight;
168
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
169
+ /* @__PURE__ */ jsx(Icon, { className: "size-3 shrink-0 text-muted-foreground", "aria-hidden": true }),
170
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
171
+ ] });
172
+ if (!onOpen) {
173
+ return /* @__PURE__ */ jsx("span", { className: CHIP, title: label, "data-testid": testId, children: body });
174
+ }
175
+ const stop = (e) => e.stopPropagation();
176
+ return /* @__PURE__ */ jsx(
177
+ "button",
178
+ {
179
+ type: "button",
180
+ title: label,
181
+ "data-testid": testId,
182
+ onClick: (e) => {
183
+ e.stopPropagation();
184
+ onOpen();
185
+ },
186
+ onDoubleClick: stop,
187
+ onMouseDown: stop,
188
+ className: cn(CHIP, "hover:bg-muted hover:underline"),
189
+ children: body
190
+ }
191
+ );
192
+ }
193
+ function ChipRow({ children }) {
194
+ return /* @__PURE__ */ jsx("span", { className: "flex h-full min-w-0 items-center gap-1 overflow-hidden", children });
195
+ }
196
+ function PersonCell({ value, titles }) {
197
+ const [id] = cellIds(value);
198
+ return id ? /* @__PURE__ */ jsx(PersonChip, { name: titleOf(id, titles) }) : null;
199
+ }
200
+ function LinkChipsCell({
201
+ value,
202
+ titles,
203
+ kind,
204
+ onOpenDocument
205
+ }) {
206
+ const ids = cellIds(value);
207
+ if (ids.length === 0) return null;
208
+ return /* @__PURE__ */ jsx(ChipRow, { children: ids.map((id) => /* @__PURE__ */ jsx(
209
+ LinkChip,
210
+ {
211
+ label: titleOf(id, titles),
212
+ kind,
213
+ testId: `database-chip-${id}`,
214
+ onOpen: kind === "document" && onOpenDocument ? () => onOpenDocument(id) : void 0
215
+ },
216
+ id
217
+ )) });
218
+ }
219
+ function LookupCell({
220
+ value,
221
+ titles,
222
+ locale
223
+ }) {
224
+ const values = cellList(value);
225
+ if (values.length === 0) return null;
226
+ return /* @__PURE__ */ jsx(ChipRow, { children: values.map((v, i) => {
227
+ const text = formatLookupValue(v, titles, locale);
228
+ const key = `${i}-${text}`;
229
+ if (typeof v === "string" && /^doc-/.test(v))
230
+ return /* @__PURE__ */ jsx(LinkChip, { label: text, kind: "document" }, key);
231
+ if (typeof v === "string" && /^dbr-/.test(v))
232
+ return /* @__PURE__ */ jsx(LinkChip, { label: text, kind: "relation" }, key);
233
+ return /* @__PURE__ */ jsx(
234
+ "span",
235
+ {
236
+ title: text,
237
+ 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",
238
+ children: text
239
+ },
240
+ key
241
+ );
242
+ }) });
243
+ }
244
+ var MAX_SHOWN = 50;
245
+ var SEARCH_DEBOUNCE_MS = 200;
246
+ var DEFAULT_PICK_CELL_EDITOR_LABELS = {
247
+ searchPlaceholder: "Search\u2026",
248
+ noMatches: "No matches",
249
+ searching: "Searching\u2026",
250
+ clear: "Clear",
251
+ done: "Done",
252
+ remove: (label) => `Remove ${label}`
253
+ };
254
+ function useChoices(query, options, search) {
255
+ const [found, setFound] = useState([]);
256
+ const [loading, setLoading] = useState(Boolean(search));
257
+ useEffect(() => {
258
+ if (!search) return;
259
+ let live = true;
260
+ setLoading(true);
261
+ const t = setTimeout(
262
+ () => {
263
+ search(query.trim()).then((rows) => live && setFound(rows)).catch(() => live && setFound([])).finally(() => live && setLoading(false));
264
+ },
265
+ query ? SEARCH_DEBOUNCE_MS : 0
266
+ );
267
+ return () => {
268
+ live = false;
269
+ clearTimeout(t);
270
+ };
271
+ }, [query, search]);
272
+ const filtered = useMemo(() => {
273
+ if (search) return found.slice(0, MAX_SHOWN);
274
+ const q = query.trim().toLowerCase();
275
+ const all = options ?? [];
276
+ const hits = q ? all.filter(
277
+ (o) => o.label.toLowerCase().includes(q) || (o.description ?? "").toLowerCase().includes(q)
278
+ ) : all;
279
+ return hits.slice(0, MAX_SHOWN);
280
+ }, [found, options, query, search]);
281
+ return { choices: filtered, loading: Boolean(search) && loading };
282
+ }
283
+ function PickCellEditor(props) {
284
+ const { value, onValueChange, stopEditing, data, kind, options, search } = props;
285
+ const l = { ...DEFAULT_PICK_CELL_EDITOR_LABELS, ...props.labels };
286
+ const multiple = kind !== "person";
287
+ const [chosen, setChosen] = useState(() => cellIds(value));
288
+ const [query, setQuery] = useState("");
289
+ const [active, setActive] = useState(0);
290
+ const input = useRef(null);
291
+ const { choices, loading } = useChoices(query, options, search);
292
+ const names = useRef({ ...data?.titles ?? {} });
293
+ for (const o of options ?? []) names.current[o.id] ??= o.label;
294
+ for (const o of choices) names.current[o.id] = o.label;
295
+ const nameOf = (id) => names.current[id] ?? id;
296
+ useEffect(() => input.current?.focus(), []);
297
+ useEffect(() => setActive(0), [query]);
298
+ const commit = (ids) => {
299
+ setChosen(ids);
300
+ if (data) {
301
+ const titles = { ...data.titles ?? {} };
302
+ for (const id of ids) titles[id] ??= nameOf(id);
303
+ data.titles = titles;
304
+ }
305
+ onValueChange(multiple ? ids : ids[0] ?? null);
306
+ };
307
+ const close = () => setTimeout(() => stopEditing(), 0);
308
+ const toggle = (id) => {
309
+ if (!multiple) {
310
+ commit([id]);
311
+ close();
312
+ return;
313
+ }
314
+ commit(chosen.includes(id) ? chosen.filter((x) => x !== id) : [...chosen, id]);
315
+ };
316
+ const onKeyDown = (e) => {
317
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
318
+ e.preventDefault();
319
+ e.stopPropagation();
320
+ if (choices.length === 0) return;
321
+ const step = e.key === "ArrowDown" ? 1 : -1;
322
+ setActive((i) => (i + step + choices.length) % choices.length);
323
+ } else if (e.key === "Enter") {
324
+ e.preventDefault();
325
+ e.stopPropagation();
326
+ const pick = choices[active];
327
+ if (pick) toggle(pick.id);
328
+ else if (multiple) close();
329
+ } else if (e.key === "Backspace" && multiple && !query && chosen.length > 0) {
330
+ commit(chosen.slice(0, -1));
331
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "Home" || e.key === "End") {
332
+ e.stopPropagation();
333
+ }
334
+ };
335
+ return /* @__PURE__ */ jsxs(
336
+ "div",
337
+ {
338
+ "data-testid": "database-pick-editor",
339
+ className: "flex w-72 flex-col rounded-md border border-border bg-popover shadow-md",
340
+ children: [
341
+ 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: [
342
+ /* @__PURE__ */ jsx(LinkChip, { label: nameOf(id), kind: kind === "document" ? "document" : "relation" }),
343
+ /* @__PURE__ */ jsx(
344
+ "button",
345
+ {
346
+ type: "button",
347
+ "aria-label": l.remove(nameOf(id)),
348
+ onClick: () => commit(chosen.filter((x) => x !== id)),
349
+ "data-testid": `database-pick-remove-${id}`,
350
+ className: "rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
351
+ children: /* @__PURE__ */ jsx(X, { className: "size-3" })
352
+ }
353
+ )
354
+ ] }, id)) }),
355
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 border-b border-border px-2", children: [
356
+ /* @__PURE__ */ jsx(Search, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
357
+ /* @__PURE__ */ jsx(
358
+ "input",
359
+ {
360
+ ref: input,
361
+ value: query,
362
+ placeholder: l.searchPlaceholder,
363
+ onChange: (e) => setQuery(e.target.value),
364
+ onKeyDown,
365
+ "data-testid": "database-pick-search",
366
+ className: "h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
367
+ }
368
+ ),
369
+ loading && /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin text-muted-foreground" })
370
+ ] }),
371
+ /* @__PURE__ */ jsxs("div", { role: "listbox", "aria-multiselectable": multiple, className: "max-h-64 overflow-y-auto p-1", children: [
372
+ choices.map((o, i) => {
373
+ const isChosen = chosen.includes(o.id);
374
+ return /* @__PURE__ */ jsxs(
375
+ "button",
376
+ {
377
+ type: "button",
378
+ role: "option",
379
+ "aria-selected": isChosen,
380
+ onMouseEnter: () => setActive(i),
381
+ onClick: () => toggle(o.id),
382
+ "data-testid": `database-pick-option-${o.id}`,
383
+ className: cn(
384
+ "flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm",
385
+ i === active && "bg-muted"
386
+ ),
387
+ children: [
388
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
389
+ kind === "person" ? /* @__PURE__ */ jsx(PersonChip, { name: o.label }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: o.label }),
390
+ o.description && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: o.description })
391
+ ] }),
392
+ isChosen && /* @__PURE__ */ jsx(Check, { className: "size-3.5 shrink-0" })
393
+ ]
394
+ },
395
+ o.id
396
+ );
397
+ }),
398
+ choices.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-2 py-1.5 text-sm text-muted-foreground", children: loading ? l.searching : l.noMatches })
399
+ ] }),
400
+ (multiple || chosen.length > 0) && /* @__PURE__ */ jsx("div", { className: "flex justify-end border-t border-border p-1", children: !multiple ? /* @__PURE__ */ jsx(
401
+ "button",
402
+ {
403
+ type: "button",
404
+ onClick: () => {
405
+ commit([]);
406
+ close();
407
+ },
408
+ "data-testid": "database-pick-clear",
409
+ className: "rounded px-2 py-1 text-sm text-muted-foreground hover:bg-muted",
410
+ children: l.clear
411
+ }
412
+ ) : /* @__PURE__ */ jsx(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ onClick: close,
417
+ "data-testid": "database-pick-done",
418
+ className: "rounded px-2 py-1 text-sm font-medium hover:bg-muted",
419
+ children: l.done
420
+ }
421
+ ) })
422
+ ]
423
+ }
424
+ );
78
425
  }
79
426
  function SelectChip({ value }) {
80
427
  return /* @__PURE__ */ jsx(
@@ -144,7 +491,8 @@ function SelectCellEditor(props) {
144
491
  }
145
492
  var DEFAULT_DATABASE_COLUMN_LABELS = {
146
493
  addColumn: "Add a column",
147
- columnSettings: (name) => `${name} column settings`
494
+ columnSettings: (name) => `${name} column settings`,
495
+ computed: "Computed from a relation (read-only)"
148
496
  };
149
497
  var ADD_COLUMN_COL_ID = "__add_column";
150
498
  var COLUMN_TYPE_ICONS = {
@@ -152,24 +500,36 @@ var COLUMN_TYPE_ICONS = {
152
500
  number: Hash,
153
501
  date: CalendarDays,
154
502
  checkbox: CheckSquare,
155
- select: ChevronDown
503
+ select: ChevronDown,
504
+ person: UserRound,
505
+ document: FileText,
506
+ relation: ArrowUpRight,
507
+ lookup: Search,
508
+ rollup: Sigma
156
509
  };
157
510
  var WIDTHS = {
158
511
  text: 220,
159
512
  number: 130,
160
513
  date: 140,
161
514
  checkbox: 110,
162
- select: 160
515
+ select: 160,
516
+ person: 180,
517
+ document: 240,
518
+ relation: 240,
519
+ lookup: 200,
520
+ rollup: 130
163
521
  };
164
522
  function HeaderLabel({
165
523
  column,
166
524
  onEdit,
167
- label
525
+ label,
526
+ computedLabel
168
527
  }) {
169
528
  const Icon = COLUMN_TYPE_ICONS[column.type];
170
529
  return /* @__PURE__ */ jsxs("span", { className: "group/dbh flex min-w-0 items-center gap-1.5", children: [
171
530
  /* @__PURE__ */ jsx(Icon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
172
531
  /* @__PURE__ */ jsx("span", { className: "truncate text-[12.5px] font-medium", children: column.name }),
532
+ 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 }) }),
173
533
  onEdit && /* @__PURE__ */ jsx(
174
534
  "button",
175
535
  {
@@ -189,9 +549,22 @@ function HeaderLabel({
189
549
  ] });
190
550
  }
191
551
  function columnWidth(column) {
192
- return Math.min(320, Math.max(WIDTHS[column.type], Math.round(column.name.length * 8) + 76));
552
+ const header = Math.round(column.name.length * 8) + 76 + (isComputedColumn(column) ? 16 : 0);
553
+ return Math.min(320, Math.max(WIDTHS[column.type], header));
193
554
  }
194
- function editorFor(column) {
555
+ function canEdit(column, sources) {
556
+ if (isComputedColumn(column)) return false;
557
+ if (column.type === "person") return Boolean(sources?.members);
558
+ if (column.type === "document") return Boolean(sources?.documents);
559
+ if (column.type === "relation") return Boolean(sources?.searchRows && column.relation);
560
+ return true;
561
+ }
562
+ function editorFor(column, sources) {
563
+ const pick = (params) => ({
564
+ cellEditor: PickCellEditor,
565
+ cellEditorPopup: true,
566
+ cellEditorParams: params
567
+ });
195
568
  switch (column.type) {
196
569
  case "number":
197
570
  return { cellEditor: "agNumberCellEditor", cellEditorParams: { showStepperButtons: false } };
@@ -205,11 +578,26 @@ function editorFor(column) {
205
578
  cellEditorPopup: true,
206
579
  cellEditorParams: { options: column.options ?? [] }
207
580
  };
581
+ case "person":
582
+ return pick({ kind: "person", options: sources?.members ?? [] });
583
+ case "document":
584
+ return pick({ kind: "document", options: sources?.documents ?? [] });
585
+ case "relation": {
586
+ const target = column.relation?.database_id;
587
+ const searchRows = sources?.searchRows;
588
+ return pick({
589
+ kind: "relation",
590
+ search: target && searchRows ? (q) => searchRows(target, q) : void 0
591
+ });
592
+ }
593
+ case "lookup":
594
+ case "rollup":
595
+ return {};
208
596
  default:
209
597
  return { cellEditor: "agTextCellEditor" };
210
598
  }
211
599
  }
212
- function rendererFor(column, locale) {
600
+ function rendererFor(column, locale, sources) {
213
601
  switch (column.type) {
214
602
  case "checkbox":
215
603
  return {
@@ -225,16 +613,42 @@ function rendererFor(column, locale) {
225
613
  cellClass: "cell-numeric",
226
614
  valueFormatter: (p) => formatCell(p.value, "number", locale)
227
615
  };
616
+ case "rollup":
617
+ return {
618
+ cellClass: "cell-numeric text-muted-foreground",
619
+ valueFormatter: (p) => formatCell(p.value, "rollup", locale)
620
+ };
228
621
  case "date":
229
622
  return {
230
623
  valueFormatter: (p) => formatCell(p.value, "date", locale)
231
624
  };
625
+ case "person":
626
+ return {
627
+ cellRenderer: (p) => /* @__PURE__ */ jsx(PersonCell, { value: p.value, titles: p.data?.titles })
628
+ };
629
+ case "document":
630
+ case "relation":
631
+ return {
632
+ cellRenderer: (p) => /* @__PURE__ */ jsx(
633
+ LinkChipsCell,
634
+ {
635
+ value: p.value,
636
+ titles: p.data?.titles,
637
+ kind: column.type === "document" ? "document" : "relation",
638
+ onOpenDocument: sources?.onOpenDocument
639
+ }
640
+ )
641
+ };
642
+ case "lookup":
643
+ return {
644
+ cellRenderer: (p) => /* @__PURE__ */ jsx(LookupCell, { value: p.value, titles: p.data?.titles, locale })
645
+ };
232
646
  default:
233
647
  return {};
234
648
  }
235
649
  }
236
650
  function createDatabaseColumns(columns, options = {}) {
237
- const { readOnly, onEditColumn, onAddColumn, locale } = options;
651
+ const { readOnly, onEditColumn, onAddColumn, locale, sources } = options;
238
652
  const l = { ...DEFAULT_DATABASE_COLUMN_LABELS, ...options.labels };
239
653
  const defs = columns.map((column) => ({
240
654
  colId: column.id,
@@ -243,7 +657,7 @@ function createDatabaseColumns(columns, options = {}) {
243
657
  minWidth: 90,
244
658
  resizable: true,
245
659
  sortable: true,
246
- editable: !readOnly,
660
+ editable: !readOnly && canEdit(column, sources),
247
661
  // The grid's own type inference must not second-guess the database's column type.
248
662
  cellDataType: false,
249
663
  headerComponentParams: {
@@ -252,6 +666,7 @@ function createDatabaseColumns(columns, options = {}) {
252
666
  {
253
667
  column,
254
668
  label: l.columnSettings(column.name),
669
+ computedLabel: l.computed,
255
670
  onEdit: !readOnly && onEditColumn ? () => onEditColumn(column) : void 0
256
671
  }
257
672
  )
@@ -263,14 +678,14 @@ function createDatabaseColumns(columns, options = {}) {
263
678
  valueSetter: (p) => {
264
679
  if (!p.data) return false;
265
680
  const next = toApiValue(p.newValue, column.type);
266
- if (next === (p.oldValue ?? null)) return false;
681
+ if (sameCellValue(next, p.oldValue)) return false;
267
682
  p.data.values = { ...p.data.values, [column.id]: next };
268
683
  return true;
269
684
  },
270
- comparator: (a, b) => compareCells(a, b, column.type),
271
- getQuickFilterText: (p) => formatCell(p.value, column.type, locale),
272
- ...editorFor(column),
273
- ...rendererFor(column, locale)
685
+ comparator: (a, b, nodeA, nodeB) => compareCells(a, b, column.type, nodeA?.data?.titles, nodeB?.data?.titles),
686
+ getQuickFilterText: (p) => formatCell(p.value, column.type, locale, p.data?.titles),
687
+ ...editorFor(column, sources),
688
+ ...rendererFor(column, locale, sources)
274
689
  }));
275
690
  if (!readOnly && onAddColumn) {
276
691
  defs.push({
@@ -302,6 +717,225 @@ function createDatabaseColumns(columns, options = {}) {
302
717
  }
303
718
  return defs;
304
719
  }
720
+ var DEFAULT_COLUMN_CONFIG_LABELS = {
721
+ relatedDatabase: "Related database",
722
+ pickDatabase: "Pick a database",
723
+ noDatabases: "There is no database to link to yet.",
724
+ targetFixed: "The related database can\u2019t be changed once the column has been added.",
725
+ thisDatabase: "this database",
726
+ showOnTarget: (target) => `Show on ${target}`,
727
+ showOnTargetHint: "Adds a column to the related database that lists the links back.",
728
+ pairName: "Name of the column there",
729
+ pairNamePlaceholder: (name) => name,
730
+ pairedWith: (target) => `Shown on ${target} too (two-way).`,
731
+ oneWay: "One-way: only this database shows the links.",
732
+ relation: "Relation",
733
+ pickRelation: "Pick a relation column",
734
+ noRelations: "Add a relation column first: lookups and rollups read through one.",
735
+ lookupColumn: "Column to show",
736
+ pickColumn: "Pick a column",
737
+ noColumns: "The related database has no column to show.",
738
+ rollupOp: "Calculate",
739
+ rollupColumn: "Of the number column",
740
+ noNumberColumns: "The related database has no number column.",
741
+ ops: { count: "Count", sum: "Sum", avg: "Average", min: "Min", max: "Max" }
742
+ };
743
+ function Field({ label, children, hint }) {
744
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
745
+ /* @__PURE__ */ jsx(Label, { children: label }),
746
+ children,
747
+ hint && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hint })
748
+ ] });
749
+ }
750
+ function Hint({ children, testId }) {
751
+ return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "data-testid": testId, children });
752
+ }
753
+ function ColumnSelect({
754
+ value,
755
+ onChange,
756
+ columns,
757
+ placeholder,
758
+ testId
759
+ }) {
760
+ return /* @__PURE__ */ jsxs(Select, { value: value || void 0, onValueChange: onChange, children: [
761
+ /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": testId, children: /* @__PURE__ */ jsx(SelectValue, { placeholder }) }),
762
+ /* @__PURE__ */ jsx(SelectContent, { children: columns.map((c) => {
763
+ const Icon = COLUMN_TYPE_ICONS[c.type];
764
+ return /* @__PURE__ */ jsx(SelectItem, { value: c.id, "data-testid": `${testId}-${c.id}`, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
765
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 text-muted-foreground" }),
766
+ c.name
767
+ ] }) }, c.id);
768
+ }) })
769
+ ] });
770
+ }
771
+ function RelationFields({
772
+ draft,
773
+ onChange,
774
+ databases,
775
+ databaseId,
776
+ databaseName,
777
+ editing,
778
+ column,
779
+ labels
780
+ }) {
781
+ const nameOf = (id) => databases.find((d) => d.id === id)?.name ?? (id === databaseId ? labels.thisDatabase : id);
782
+ if (editing && column?.relation) {
783
+ const target2 = nameOf(column.relation.database_id);
784
+ return /* @__PURE__ */ jsxs(Field, { label: labels.relatedDatabase, hint: labels.targetFixed, children: [
785
+ /* @__PURE__ */ jsx("p", { className: "text-sm", "data-testid": "database-column-relation-target", children: target2 }),
786
+ /* @__PURE__ */ jsx(Hint, { children: column.relation.two_way ? labels.pairedWith(target2) : labels.oneWay })
787
+ ] });
788
+ }
789
+ if (databases.length === 0) return /* @__PURE__ */ jsx(Hint, { children: labels.noDatabases });
790
+ const target = draft.database_id ? nameOf(draft.database_id) : "";
791
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
792
+ /* @__PURE__ */ jsx(Field, { label: labels.relatedDatabase, children: /* @__PURE__ */ jsxs(
793
+ Select,
794
+ {
795
+ value: draft.database_id || void 0,
796
+ onValueChange: (v) => onChange({ ...draft, database_id: v }),
797
+ children: [
798
+ /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": "database-column-relation-database", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: labels.pickDatabase }) }),
799
+ /* @__PURE__ */ jsx(SelectContent, { children: databases.map((d) => /* @__PURE__ */ jsx(
800
+ SelectItem,
801
+ {
802
+ value: d.id,
803
+ "data-testid": `database-column-relation-database-${d.id}`,
804
+ children: d.id === databaseId ? `${d.name} (${labels.thisDatabase})` : d.name
805
+ },
806
+ d.id
807
+ )) })
808
+ ]
809
+ }
810
+ ) }),
811
+ draft.database_id && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-border p-3", children: [
812
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-3 text-sm", children: [
813
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col", children: [
814
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: labels.showOnTarget(target) }),
815
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: labels.showOnTargetHint })
816
+ ] }),
817
+ /* @__PURE__ */ jsx(
818
+ Switch,
819
+ {
820
+ checked: draft.two_way,
821
+ onCheckedChange: (two_way) => onChange({ ...draft, two_way }),
822
+ "data-testid": "database-column-relation-two-way"
823
+ }
824
+ )
825
+ ] }),
826
+ draft.two_way && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
827
+ /* @__PURE__ */ jsx(Label, { htmlFor: "database-column-pair-name", children: labels.pairName }),
828
+ /* @__PURE__ */ jsx(
829
+ Input,
830
+ {
831
+ id: "database-column-pair-name",
832
+ value: draft.pair_name,
833
+ maxLength: 200,
834
+ placeholder: labels.pairNamePlaceholder(databaseName ?? ""),
835
+ onChange: (e) => onChange({ ...draft, pair_name: e.target.value }),
836
+ "data-testid": "database-column-pair-name"
837
+ }
838
+ )
839
+ ] })
840
+ ] })
841
+ ] });
842
+ }
843
+ function relationColumnsOf(columns) {
844
+ return columns.filter((c) => c.type === "relation" && c.relation);
845
+ }
846
+ function targetColumnsOf(relationId, columns, databases, databaseId) {
847
+ const relation = columns.find((c) => c.id === relationId)?.relation;
848
+ if (!relation) return [];
849
+ if (relation.database_id === databaseId) return columns;
850
+ return databases.find((d) => d.id === relation.database_id)?.columns ?? [];
851
+ }
852
+ function LookupFields({
853
+ draft,
854
+ onChange,
855
+ columns,
856
+ databases,
857
+ databaseId,
858
+ labels
859
+ }) {
860
+ const relations = relationColumnsOf(columns);
861
+ if (relations.length === 0)
862
+ return /* @__PURE__ */ jsx(Hint, { testId: "database-column-no-relations", children: labels.noRelations });
863
+ const shown = targetColumnsOf(draft.relation, columns, databases, databaseId).filter(
864
+ (c) => !isComputedColumn(c)
865
+ );
866
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
867
+ /* @__PURE__ */ jsx(Field, { label: labels.relation, children: /* @__PURE__ */ jsx(
868
+ ColumnSelect,
869
+ {
870
+ value: draft.relation,
871
+ onChange: (relation) => onChange({ relation, column: "" }),
872
+ columns: relations,
873
+ placeholder: labels.pickRelation,
874
+ testId: "database-column-lookup-relation"
875
+ }
876
+ ) }),
877
+ draft.relation && /* @__PURE__ */ jsx(Field, { label: labels.lookupColumn, children: shown.length ? /* @__PURE__ */ jsx(
878
+ ColumnSelect,
879
+ {
880
+ value: draft.column,
881
+ onChange: (column) => onChange({ ...draft, column }),
882
+ columns: shown,
883
+ placeholder: labels.pickColumn,
884
+ testId: "database-column-lookup-column"
885
+ }
886
+ ) : /* @__PURE__ */ jsx(Hint, { children: labels.noColumns }) })
887
+ ] });
888
+ }
889
+ function RollupFields({
890
+ draft,
891
+ onChange,
892
+ columns,
893
+ databases,
894
+ databaseId,
895
+ labels
896
+ }) {
897
+ const relations = relationColumnsOf(columns);
898
+ if (relations.length === 0)
899
+ return /* @__PURE__ */ jsx(Hint, { testId: "database-column-no-relations", children: labels.noRelations });
900
+ const numbers = targetColumnsOf(draft.relation, columns, databases, databaseId).filter(
901
+ (c) => c.type === "number"
902
+ );
903
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
904
+ /* @__PURE__ */ jsx(Field, { label: labels.relation, children: /* @__PURE__ */ jsx(
905
+ ColumnSelect,
906
+ {
907
+ value: draft.relation,
908
+ onChange: (relation) => onChange({ ...draft, relation, column: "" }),
909
+ columns: relations,
910
+ placeholder: labels.pickRelation,
911
+ testId: "database-column-rollup-relation"
912
+ }
913
+ ) }),
914
+ draft.relation && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[8rem_1fr] gap-2", children: [
915
+ /* @__PURE__ */ jsx(Field, { label: labels.rollupOp, children: /* @__PURE__ */ jsxs(
916
+ Select,
917
+ {
918
+ value: draft.op,
919
+ onValueChange: (op) => onChange({ ...draft, op }),
920
+ children: [
921
+ /* @__PURE__ */ jsx(SelectTrigger, { "data-testid": "database-column-rollup-op", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
922
+ /* @__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)) })
923
+ ]
924
+ }
925
+ ) }),
926
+ draft.op !== "count" && /* @__PURE__ */ jsx(Field, { label: labels.rollupColumn, children: numbers.length ? /* @__PURE__ */ jsx(
927
+ ColumnSelect,
928
+ {
929
+ value: draft.column,
930
+ onChange: (column) => onChange({ ...draft, column }),
931
+ columns: numbers,
932
+ placeholder: labels.pickColumn,
933
+ testId: "database-column-rollup-column"
934
+ }
935
+ ) : /* @__PURE__ */ jsx(Hint, { children: labels.noNumberColumns }) })
936
+ ] })
937
+ ] });
938
+ }
305
939
  var escapeHtml = (text) => text.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
306
940
  var DEFAULT_DATABASE_TABLE_LABELS = {
307
941
  newRow: "New row",
@@ -318,6 +952,7 @@ function DatabaseTable({
318
952
  onAddRow,
319
953
  onAddColumn,
320
954
  onEditColumn,
955
+ sources,
321
956
  onSelectionChange,
322
957
  selected,
323
958
  readOnly = false,
@@ -337,10 +972,11 @@ function DatabaseTable({
337
972
  readOnly,
338
973
  onEditColumn,
339
974
  onAddColumn,
975
+ sources,
340
976
  locale,
341
977
  labels: columnLabels
342
978
  }),
343
- [columns, readOnly, onEditColumn, onAddColumn, locale, columnLabels]
979
+ [columns, readOnly, onEditColumn, onAddColumn, sources, locale, columnLabels]
344
980
  );
345
981
  const noColumns = columns.length === 0;
346
982
  const emptyState = {
@@ -660,25 +1296,45 @@ var DEFAULT_DATABASE_COLUMN_DIALOG_LABELS = {
660
1296
  delete: "Delete column",
661
1297
  confirmDelete: "Delete this column and every value in it?"
662
1298
  };
1299
+ var NO_RELATION = { database_id: "", two_way: false, pair_name: "" };
1300
+ var NO_LOOKUP = { relation: "", column: "" };
1301
+ var NO_ROLLUP = { relation: "", column: "", op: "count" };
1302
+ var NO_COLUMNS = [];
1303
+ var NO_DATABASES = [];
1304
+ function configComplete(type, editing, relation, lookup, rollup) {
1305
+ if (type === "relation") return editing || Boolean(relation.database_id);
1306
+ if (type === "lookup") return Boolean(lookup.relation && lookup.column);
1307
+ if (type === "rollup") return Boolean(rollup.relation && (rollup.op === "count" || rollup.column));
1308
+ return true;
1309
+ }
663
1310
  function DatabaseColumnDialog({
664
1311
  open,
665
1312
  onOpenChange,
666
1313
  column,
1314
+ databaseId,
1315
+ databaseName,
1316
+ columns = NO_COLUMNS,
1317
+ databases = NO_DATABASES,
667
1318
  onSave,
668
1319
  onDelete,
669
1320
  isSaving = false,
670
1321
  error,
671
1322
  labels,
672
- typeLabels
1323
+ typeLabels,
1324
+ configLabels
673
1325
  }) {
674
1326
  const l = { ...DEFAULT_DATABASE_COLUMN_DIALOG_LABELS, ...labels };
675
1327
  const tl = { ...DEFAULT_DATABASE_COLUMN_TYPE_LABELS, ...typeLabels };
1328
+ const cl = { ...DEFAULT_COLUMN_CONFIG_LABELS, ...configLabels };
676
1329
  const editing = Boolean(column);
677
1330
  const [name, setName] = useState("");
678
1331
  const [type, setType] = useState("text");
679
1332
  const [options, setOptions] = useState([]);
680
1333
  const [optionDraft, setOptionDraft] = useState("");
681
1334
  const [confirmingDelete, setConfirmingDelete] = useState(false);
1335
+ const [relation, setRelation] = useState(NO_RELATION);
1336
+ const [lookup, setLookup] = useState(NO_LOOKUP);
1337
+ const [rollup, setRollup] = useState(NO_ROLLUP);
682
1338
  useEffect(() => {
683
1339
  if (!open) return;
684
1340
  setName(column?.name ?? "");
@@ -686,6 +1342,15 @@ function DatabaseColumnDialog({
686
1342
  setOptions(column?.options ?? []);
687
1343
  setOptionDraft("");
688
1344
  setConfirmingDelete(false);
1345
+ setRelation(NO_RELATION);
1346
+ setLookup(column?.lookup ? { ...column.lookup } : NO_LOOKUP);
1347
+ setRollup(
1348
+ column?.rollup ? {
1349
+ relation: column.rollup.relation,
1350
+ column: column.rollup.column ?? "",
1351
+ op: column.rollup.op ?? "count"
1352
+ } : NO_ROLLUP
1353
+ );
689
1354
  }, [open, column]);
690
1355
  const addOption = () => {
691
1356
  const option = optionDraft.trim();
@@ -696,15 +1361,29 @@ function DatabaseColumnDialog({
696
1361
  };
697
1362
  const pendingOptions = optionDraft.trim() ? [...options, optionDraft.trim()] : options;
698
1363
  const missingOptions = type === "select" && pendingOptions.length === 0;
699
- const canSave = name.trim().length > 0 && !missingOptions && !isSaving;
1364
+ const canSave = name.trim().length > 0 && !missingOptions && configComplete(type, editing, relation, lookup, rollup) && !isSaving;
700
1365
  const submit = (e) => {
701
1366
  e.preventDefault();
702
1367
  if (!canSave) return;
703
- onSave({
1368
+ const draft = {
704
1369
  name: name.trim(),
705
1370
  type,
706
1371
  options: type === "select" ? Array.from(new Set(pendingOptions)) : null
707
- });
1372
+ };
1373
+ if (type === "relation" && !editing)
1374
+ draft.relation = {
1375
+ database_id: relation.database_id,
1376
+ two_way: relation.two_way,
1377
+ pair_name: relation.two_way ? relation.pair_name.trim() || null : null
1378
+ };
1379
+ if (type === "lookup") draft.lookup = { ...lookup };
1380
+ if (type === "rollup")
1381
+ draft.rollup = {
1382
+ relation: rollup.relation,
1383
+ op: rollup.op,
1384
+ column: rollup.op === "count" ? null : rollup.column
1385
+ };
1386
+ onSave(draft);
708
1387
  };
709
1388
  const onOptionKey = (e) => {
710
1389
  if (e.key === "Enter") {
@@ -795,6 +1474,41 @@ function DatabaseColumnDialog({
795
1474
  ] }),
796
1475
  missingOptions && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: l.needsOptions })
797
1476
  ] }),
1477
+ type === "relation" && /* @__PURE__ */ jsx(
1478
+ RelationFields,
1479
+ {
1480
+ draft: relation,
1481
+ onChange: setRelation,
1482
+ databases,
1483
+ databaseId,
1484
+ databaseName,
1485
+ editing,
1486
+ column,
1487
+ labels: cl
1488
+ }
1489
+ ),
1490
+ type === "lookup" && /* @__PURE__ */ jsx(
1491
+ LookupFields,
1492
+ {
1493
+ draft: lookup,
1494
+ onChange: setLookup,
1495
+ columns,
1496
+ databases,
1497
+ databaseId,
1498
+ labels: cl
1499
+ }
1500
+ ),
1501
+ type === "rollup" && /* @__PURE__ */ jsx(
1502
+ RollupFields,
1503
+ {
1504
+ draft: rollup,
1505
+ onChange: setRollup,
1506
+ columns,
1507
+ databases,
1508
+ databaseId,
1509
+ labels: cl
1510
+ }
1511
+ ),
798
1512
  error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-red-text", "data-testid": "database-column-error", children: error }),
799
1513
  /* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row items-center gap-2 sm:justify-between", children: [
800
1514
  editing && onDelete ? confirmingDelete ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm", children: [
@@ -1050,6 +1764,6 @@ function NewDatabaseDialog({
1050
1764
  ] }) }) });
1051
1765
  }
1052
1766
 
1053
- export { ADD_COLUMN_COL_ID, COLUMN_TYPE_ICONS, DATABASE_COLUMN_TYPES, DEFAULT_DATABASE_COLUMN_DIALOG_LABELS, DEFAULT_DATABASE_COLUMN_LABELS, DEFAULT_DATABASE_COLUMN_TYPE_LABELS, DEFAULT_DATABASE_LIST_LABELS, DEFAULT_DATABASE_TABLE_LABELS, DEFAULT_DATABASE_TOOLBAR_LABELS, DEFAULT_NEW_DATABASE_DIALOG_LABELS, DatabaseColumnDialog, DatabaseList, DatabaseTable, DatabaseToolbar, NewDatabaseDialog, SelectCellEditor, SelectChip, columnWidth, compareCells, createDatabaseColumns, formatCell, formatDateValue, formatNumberValue, optionTone, toApiValue };
1767
+ 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_TABLE_LABELS, DEFAULT_DATABASE_TOOLBAR_LABELS, DEFAULT_NEW_DATABASE_DIALOG_LABELS, DEFAULT_PICK_CELL_EDITOR_LABELS, DatabaseColumnDialog, DatabaseList, DatabaseTable, DatabaseToolbar, LINK_COLUMN_TYPES, LinkChip, LinkChipsCell, LookupCell, NewDatabaseDialog, PersonCell, PersonChip, PickCellEditor, SelectCellEditor, SelectChip, cellIds, cellList, columnWidth, compareCells, createDatabaseColumns, formatCell, formatDateValue, formatLookupValue, formatNumberValue, isComputedColumn, optionTone, sameCellValue, titleOf, toApiValue };
1054
1768
  //# sourceMappingURL=index.js.map
1055
1769
  //# sourceMappingURL=index.js.map