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