@arbidocs/blocks 0.5.37 → 0.5.38

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