@burdenoff/fluidgrids-fe-sdk 2026.1111.16 → 2026.1111.18

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 (119) hide show
  1. package/README.md +41 -0
  2. package/dist/AiStudioPage-BHVEU4Wp.js +774 -0
  3. package/dist/ApprovalInboxPage-dbBDSJ-f.js +558 -0
  4. package/dist/DataSinkDashboardPage-1h2QsMzH.js +270 -0
  5. package/dist/DataTablesPage-BZy2z0LB.js +1690 -0
  6. package/dist/EntityTagPicker-BqRNsLCQ.js +135 -0
  7. package/dist/{ExecutionDetailPage-D-0_POAf.js → ExecutionDetailPage-BdcZmLT4.js} +799 -4420
  8. package/dist/ExecutionDetailPage-DvQ667OE.d.ts +10 -0
  9. package/dist/ExecutionsListPage-BCUJoAIE.js +2742 -0
  10. package/dist/{FailureTriagePanel-BHiduL7A.js → FailureTriagePanel-B9hDQymi.js} +4 -97
  11. package/dist/{FluidGridsProvider-DmvFADFa.js → FluidGridsProvider-dvZSNtSW.js} +10 -350
  12. package/dist/HistoryPage-BLoRpJ1R.d.ts +6 -0
  13. package/dist/{HistoryPage-BbUgxx1I.js → HistoryPage-U0tqupBN.js} +6 -4
  14. package/dist/HumanRequestCard-D-ooVPDn.js +439 -0
  15. package/dist/{InsightsPage-s6f5bU-8.js → InsightsPage-CALo16eB.js} +17 -328
  16. package/dist/ManagedAiPolicyAuthority-BxwJ3ptd.js +40 -0
  17. package/dist/{ManagedAiPolicyAuthority-CGux_DaV.d.ts → ManagedAiPolicyAuthority-DM3t3wNr.d.ts} +1 -5
  18. package/dist/ManagedAiPolicyAuthorityContext-CNkxIOZ1.js +10 -0
  19. package/dist/{MissingNodesInstallModal-B-9gHy2K.js → MissingNodesInstallModal-CMybsI8h.js} +3 -2
  20. package/dist/{NodesPage-ClvhUfbh.js → NodesPage-Bf5PhfBK.js} +7 -3
  21. package/dist/NodesPage-DEbnz9iU.d.ts +6 -0
  22. package/dist/{PublishCustomNodeAction-D2F7oN0S.js → PublishCustomNodeAction-5XXnSA3H.js} +3 -45
  23. package/dist/QuotaExhaustedDialog-7IFSaLyi.js +54 -0
  24. package/dist/RunnerStatusPill-Ctg4TJcP.js +187 -0
  25. package/dist/RunnersPage-CLCOCclc.js +467 -0
  26. package/dist/{ManagedAiPolicyAuthority-DsSaz-vg.js → SettingsPage-CJLZvv4J.js} +743 -773
  27. package/dist/SettingsPage-DMHQFj5-.d.ts +9 -0
  28. package/dist/TagFilterSelect-DCBOH6Ou.js +41 -0
  29. package/dist/TaskQueuesPage-9VgSnLdL.js +951 -0
  30. package/dist/TemplateCard-DsC0KNH1.js +1416 -0
  31. package/dist/{TemplateConfigurationModal-D_Mvgxqa.js → TemplateConfigurationModal-BhGWIzpm.js} +3 -2
  32. package/dist/{TemplateDetailPanel-DfFfUcOe.js → TemplateDetailPanel-D4cUVr-m.js} +3 -3
  33. package/dist/TemplatePreview-FayF6jxh.js +231 -0
  34. package/dist/TemplatesPage-BG3H-6v6.d.ts +10 -0
  35. package/dist/{TemplatesPage-Cku13bav.js → TemplatesPage-CsDNT54N.js} +11 -228
  36. package/dist/VariablesPage-67WR_lVE.js +949 -0
  37. package/dist/{VersionDiffDialog-Dq2xlPFI.js → VersionDiffDialog-DSN0mU0f.js} +2 -1
  38. package/dist/WorkflowBuilder-CPnXEL5J.js +555 -0
  39. package/dist/{WorkflowSearchRedirect-CaoOOpEl.js → WorkflowBuilder-YXRysvZc.js} +19 -585
  40. package/dist/{WorkflowProposalCard-8Vr56kQa.js → WorkflowProposalCard-qrEeNyYG.js} +4 -405
  41. package/dist/WorkflowSearchRedirect-DwDNZ_q5.js +36 -0
  42. package/dist/WorkflowShareDialog-mbIwRQUY.js +168 -0
  43. package/dist/WorkflowsList-CcNDQge5.d.ts +6 -0
  44. package/dist/{WorkflowsList-B2j1Pehb.js → WorkflowsList-DiiGyZkT.js} +12 -6
  45. package/dist/agent-form-Bfvm32j1.js +83 -0
  46. package/dist/{agentTools-DJ5o2ekg.js → agentTools-8s_25Q_F.js} +3 -2
  47. package/dist/{ai-usage-D5p7ZeRR.js → ai-usage-glCwHyQz.js} +1 -1
  48. package/dist/api-BrCHB5oA.js +224 -0
  49. package/dist/api-C8e0iV-Q.js +55 -0
  50. package/dist/button-DG7y_FVJ.js +48 -0
  51. package/dist/{canvasNoteTools-D3zUdEeg.js → canvasNoteTools-B6pYARgU.js} +2 -2
  52. package/dist/client-C9XSzSXr.js +1147 -0
  53. package/dist/code-editor-field-9-GdVE_8.js +111 -0
  54. package/dist/{codec-client-DstqyzTN.js → codec-client-CJ95Rmqo.js} +1 -1
  55. package/dist/constants-B2lhWNoN.js +202 -0
  56. package/dist/constants-XdvUxeWK.d.ts +162 -0
  57. package/dist/dialog-Cjb4lB4D.js +105 -0
  58. package/dist/executions.d.ts +1 -1
  59. package/dist/executions.js +2 -1
  60. package/dist/feature-flags-DM5myWaD.js +296 -0
  61. package/dist/flags.d.ts +3 -0
  62. package/dist/flags.js +4 -0
  63. package/dist/{flow-builder-DEzT8TQu.js → flow-builder-CZDKRc6f.js} +662 -10716
  64. package/dist/{node-endpoint-configs-DHFGA5MQ.js → graphql-operation-types-kX-MMjgW.js} +1 -1194
  65. package/dist/{graphql-types-XcDLtFwU.js → graphql-types-CDKVWH2R.js} +2 -202
  66. package/dist/history.d.ts +1 -1
  67. package/dist/history.js +1 -1
  68. package/dist/{index-DRf25-D3.d.ts → index-6JR2vD9J.d.ts} +33 -215
  69. package/dist/index-72XFgvbn.d.ts +132 -0
  70. package/dist/index-BcdtKBM-.d.ts +56 -0
  71. package/dist/{index-B3BWqafN.d.ts → index-DjiFgEQl.d.ts} +5 -131
  72. package/dist/index.d.ts +13 -9
  73. package/dist/index.js +52 -5300
  74. package/dist/{node-detail-view-LNqeIqN-.js → node-detail-view-CV4ZRwqh.js} +13 -8
  75. package/dist/node-editor-tab-body-DU-dh35X.js +174 -0
  76. package/dist/node-endpoint-configs-B5bjNMDW.js +37 -0
  77. package/dist/node-registry-refresh-Dy0uRg9f.js +84 -0
  78. package/dist/nodes.d.ts +1 -1
  79. package/dist/nodes.js +1 -1
  80. package/dist/observability.d.ts +1 -1
  81. package/dist/observability.js +2 -1
  82. package/dist/popover-BJHvrFGR.js +472 -0
  83. package/dist/{quick-config-panel-D3ogsaFL.js → quick-config-panel-ws3pLvbt.js} +3 -2
  84. package/dist/rolldown-runtime-hDbaFkMy.js +18 -0
  85. package/dist/run-display-BycKYSU0.js +46 -0
  86. package/dist/run-list-links-ByYC0OlV.js +98 -0
  87. package/dist/schedule-panel-C0mfGQSD.js +7542 -0
  88. package/dist/settings.d.ts +2 -1
  89. package/dist/settings.js +2 -1
  90. package/dist/shared-CMfn30xT.js +196 -0
  91. package/dist/sidebar-node-editor-CR1PiZO-.js +177 -0
  92. package/dist/templates.d.ts +1 -1
  93. package/dist/templates.js +2 -1
  94. package/dist/theme-provider-B3mIuz2d.js +11 -0
  95. package/dist/{useAgentDefinitions-NFtLytDR.js → useAgentDefinitions-B0dam3_o.js} +3 -397
  96. package/dist/useConnections-Cs7LCWEM.js +401 -0
  97. package/dist/useDurableParity-CzUQMFJ3.js +415 -0
  98. package/dist/useEntityTags-DhnsZcTl.js +236 -0
  99. package/dist/{useExecutions-DAzeWHS6.js → useExecutions-9aLuOt1c.js} +4 -3
  100. package/dist/{useObservability-COCZ0hUS.js → useObservability-CfEw3ZFD.js} +2 -2
  101. package/dist/{useRunHistory-CsYUbouO.js → useRunHistory-B1xJDwl1.js} +4 -3
  102. package/dist/{WorkflowShareDialog-6RRDe-_v.js → useWorkflowCodeExport-Bu2QYJzA.js} +7 -210
  103. package/dist/{useWorkflows-w4Dx2fF2.js → useWorkflows-BlVVu8NM.js} +3 -2
  104. package/dist/utils-uaL5UVxb.js +65 -0
  105. package/dist/workflow-builder.d.ts +1 -1
  106. package/dist/workflow-builder.js +4 -2
  107. package/dist/workflow-host.d.ts +6 -0
  108. package/dist/workflow-host.js +9 -0
  109. package/dist/workflow-pages.d.ts +55 -0
  110. package/dist/workflow-pages.js +113 -0
  111. package/dist/workflows-list.d.ts +1 -1
  112. package/dist/workflows-list.js +1 -1
  113. package/package.json +13 -1
  114. package/dist/ExecutionDetailPage-MvsnOnE1.d.ts +0 -4
  115. package/dist/HistoryPage-BqUizaFv.d.ts +0 -3
  116. package/dist/NodesPage-C97-9K2Y.d.ts +0 -3
  117. package/dist/TemplatesPage-D0qnXzHh.d.ts +0 -4
  118. package/dist/WorkflowsList-0d5GSKdh.d.ts +0 -3
  119. package/dist/agent-form-BvMWhQ42.js +0 -496
@@ -0,0 +1,1690 @@
1
+ "use client";
2
+ import { t as __exportAll } from "./rolldown-runtime-hDbaFkMy.js";
3
+ import { t as EntityTagPicker } from "./EntityTagPicker-BqRNsLCQ.js";
4
+ import { t as TagFilterSelect } from "./TagFilterSelect-DCBOH6Ou.js";
5
+ import { _ as throwIfResultError, m as describeOperationError, n as getFluidGridsClient } from "./client-C9XSzSXr.js";
6
+ import { a as useDataTableTagOverlay, n as tagLabel } from "./useEntityTags-DhnsZcTl.js";
7
+ import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
8
+ import { i as SdkPermissionButton, s as useFluidGridsPermission, t as FLUIDGRIDS_PERMISSIONS } from "./constants-B2lhWNoN.js";
9
+ import { t as FluidGridsDocsLink } from "./FluidGridsDocsLink-JxkBpOeb.js";
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { useNavigate } from "react-router";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ import { toast } from "sonner";
14
+ import { gql } from "@apollo/client";
15
+ import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, IllustratedEmptyState, Input, Label, NextSteps, PagePurpose, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@burdenoff/fe-libs/ui";
16
+ import { Button as Button$1 } from "@burdenoff/fe-libs/button";
17
+ import { ArrowDownToLine, ArrowLeft, ArrowUpFromLine, ChevronLeft, ChevronRight, Database, Pencil, Plus, RefreshCw, Search, Table2, Trash2 } from "lucide-react";
18
+ import { TagBadge } from "@burdenoff/fe-libs/tag";
19
+
20
+ // src/modules/data-tables/hooks/useDataTables.ts
21
+ const DATA_TABLE_FIELDS = gql`
22
+ fragment DataTableFields on DataTable {
23
+ id
24
+ workspaceId
25
+ name
26
+ description
27
+ columns {
28
+ key
29
+ label
30
+ type
31
+ required
32
+ defaultValue
33
+ }
34
+ rowCount
35
+ sizeBytes
36
+ createdAt
37
+ updatedAt
38
+ createdBy
39
+ updatedBy
40
+ }
41
+ `;
42
+ const DATA_TABLE_ROW_FIELDS = gql`
43
+ fragment DataTableRowFields on DataTableRow {
44
+ id
45
+ tableId
46
+ data
47
+ createdAt
48
+ updatedAt
49
+ }
50
+ `;
51
+ const GET_DATA_TABLES = gql`
52
+ ${DATA_TABLE_FIELDS}
53
+ query GetDataTables($search: String, $limit: Int, $offset: Int) {
54
+ dataTables(search: $search, limit: $limit, offset: $offset) {
55
+ tables {
56
+ ...DataTableFields
57
+ }
58
+ total
59
+ }
60
+ }
61
+ `;
62
+ const GET_DATA_TABLES_BY_TAG = gql`
63
+ ${DATA_TABLE_FIELDS}
64
+ query GetDataTablesByTag(
65
+ $search: String
66
+ $limit: Int
67
+ $offset: Int
68
+ $tagIds: [ID!]
69
+ ) {
70
+ dataTables(
71
+ search: $search
72
+ limit: $limit
73
+ offset: $offset
74
+ tagIds: $tagIds
75
+ ) {
76
+ tables {
77
+ ...DataTableFields
78
+ }
79
+ total
80
+ }
81
+ }
82
+ `;
83
+ const GET_DATA_TABLE_ROWS = gql`
84
+ ${DATA_TABLE_ROW_FIELDS}
85
+ query GetDataTableRows($input: DataTableRowsQueryInput!) {
86
+ dataTableRows(input: $input) {
87
+ rows {
88
+ ...DataTableRowFields
89
+ }
90
+ total
91
+ }
92
+ }
93
+ `;
94
+ const CREATE_DATA_TABLE = gql`
95
+ ${DATA_TABLE_FIELDS}
96
+ mutation CreateDataTable($input: CreateDataTableInput!) {
97
+ createDataTable(input: $input) {
98
+ success
99
+ message
100
+ table {
101
+ ...DataTableFields
102
+ }
103
+ }
104
+ }
105
+ `;
106
+ const UPDATE_DATA_TABLE = gql`
107
+ ${DATA_TABLE_FIELDS}
108
+ mutation UpdateDataTable($id: ID!, $input: UpdateDataTableInput!) {
109
+ updateDataTable(id: $id, input: $input) {
110
+ success
111
+ message
112
+ table {
113
+ ...DataTableFields
114
+ }
115
+ }
116
+ }
117
+ `;
118
+ const DELETE_DATA_TABLE = gql`
119
+ mutation DeleteDataTable($id: ID!) {
120
+ deleteDataTable(id: $id) {
121
+ success
122
+ message
123
+ }
124
+ }
125
+ `;
126
+ const INSERT_DATA_TABLE_ROW = gql`
127
+ ${DATA_TABLE_ROW_FIELDS}
128
+ mutation InsertDataTableRow($tableId: ID!, $data: JSON!) {
129
+ insertDataTableRow(tableId: $tableId, data: $data) {
130
+ success
131
+ message
132
+ row {
133
+ ...DataTableRowFields
134
+ }
135
+ }
136
+ }
137
+ `;
138
+ const UPDATE_DATA_TABLE_ROW = gql`
139
+ ${DATA_TABLE_ROW_FIELDS}
140
+ mutation UpdateDataTableRow($id: ID!, $data: JSON!) {
141
+ updateDataTableRow(id: $id, data: $data) {
142
+ success
143
+ message
144
+ row {
145
+ ...DataTableRowFields
146
+ }
147
+ }
148
+ }
149
+ `;
150
+ const DELETE_DATA_TABLE_ROWS = gql`
151
+ mutation DeleteDataTableRows($tableId: ID!, $ids: [ID!]!) {
152
+ deleteDataTableRows(tableId: $tableId, ids: $ids) {
153
+ success
154
+ message
155
+ affected
156
+ }
157
+ }
158
+ `;
159
+ function toRow(row) {
160
+ return {
161
+ id: row.id,
162
+ tableId: row.tableId,
163
+ data: toRecord(row.data),
164
+ createdAt: row.createdAt,
165
+ updatedAt: row.updatedAt
166
+ };
167
+ }
168
+ function toRecord(value) {
169
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
170
+ }
171
+ function operationError(error, fallback) {
172
+ return new Error(describeOperationError(error, fallback));
173
+ }
174
+ /** The workspace's data tables, optionally narrowed by name and by one tag (G47). */
175
+ function useDataTables(search, tagId) {
176
+ const [tables, setTables] = useState([]);
177
+ const [total, setTotal] = useState(0);
178
+ const [loading, setLoading] = useState(true);
179
+ const [error, setError] = useState(null);
180
+ const fetchTables = useCallback(async () => {
181
+ setLoading(true);
182
+ setError(null);
183
+ try {
184
+ const result = throwIfResultError(await getFluidGridsClient().query({
185
+ query: tagId ? GET_DATA_TABLES_BY_TAG : GET_DATA_TABLES,
186
+ variables: {
187
+ search: search?.trim() || void 0,
188
+ limit: 200,
189
+ offset: 0,
190
+ ...tagId ? { tagIds: [tagId] } : {}
191
+ },
192
+ fetchPolicy: "network-only"
193
+ }));
194
+ setTables(result.data?.dataTables.tables ?? []);
195
+ setTotal(result.data?.dataTables.total ?? 0);
196
+ } catch (err) {
197
+ setError(operationError(err, "Failed to load data tables"));
198
+ } finally {
199
+ setLoading(false);
200
+ }
201
+ }, [search, tagId]);
202
+ useEffect(() => {
203
+ fetchTables();
204
+ }, [fetchTables]);
205
+ return {
206
+ tables,
207
+ total,
208
+ loading,
209
+ error,
210
+ refetch: fetchTables
211
+ };
212
+ }
213
+ /** Largest page the service serves for a single `dataTableRows` request. */
214
+ const DATA_TABLE_MAX_PAGE_SIZE = 5e3;
215
+ async function queryRowsPage(tableId, limit, offset) {
216
+ const response = throwIfResultError(await getFluidGridsClient().query({
217
+ query: GET_DATA_TABLE_ROWS,
218
+ variables: { input: {
219
+ tableId,
220
+ limit,
221
+ offset
222
+ } },
223
+ fetchPolicy: "network-only"
224
+ })).data?.dataTableRows;
225
+ return {
226
+ rows: response?.rows.map(toRow) ?? [],
227
+ total: response?.total ?? 0
228
+ };
229
+ }
230
+ /**
231
+ * Fetch every row of a table in service-sized batches (used by CSV export so
232
+ * it covers the whole table rather than the page currently on screen). A table
233
+ * is capped at 50k rows server-side, so this is at most ten requests.
234
+ */
235
+ async function fetchAllDataTableRows(tableId) {
236
+ const collected = [];
237
+ let total = Number.POSITIVE_INFINITY;
238
+ while (collected.length < total) {
239
+ const page = await queryRowsPage(tableId, DATA_TABLE_MAX_PAGE_SIZE, collected.length);
240
+ total = page.total;
241
+ if (page.rows.length === 0) break;
242
+ collected.push(...page.rows);
243
+ }
244
+ return collected;
245
+ }
246
+ function useDataTableRows(tableId, paging) {
247
+ const { page, pageSize } = paging;
248
+ const [rows, setRows] = useState([]);
249
+ const [total, setTotal] = useState(0);
250
+ const [loading, setLoading] = useState(false);
251
+ const [error, setError] = useState(null);
252
+ const fetchRows = useCallback(async () => {
253
+ if (!tableId) {
254
+ setRows([]);
255
+ setTotal(0);
256
+ setLoading(false);
257
+ setError(null);
258
+ return;
259
+ }
260
+ setLoading(true);
261
+ setError(null);
262
+ try {
263
+ const response = await queryRowsPage(tableId, pageSize, page * pageSize);
264
+ setRows(response.rows);
265
+ setTotal(response.total);
266
+ } catch (err) {
267
+ setError(operationError(err, "Failed to load data table rows"));
268
+ } finally {
269
+ setLoading(false);
270
+ }
271
+ }, [
272
+ tableId,
273
+ page,
274
+ pageSize
275
+ ]);
276
+ useEffect(() => {
277
+ fetchRows();
278
+ }, [fetchRows]);
279
+ return {
280
+ rows,
281
+ total,
282
+ loading,
283
+ error,
284
+ refetch: fetchRows
285
+ };
286
+ }
287
+ function useCreateDataTable() {
288
+ const [loading, setLoading] = useState(false);
289
+ return {
290
+ createDataTable: useCallback(async (input) => {
291
+ setLoading(true);
292
+ try {
293
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
294
+ mutation: CREATE_DATA_TABLE,
295
+ variables: { input }
296
+ })).data?.createDataTable;
297
+ if (!response?.success) throw new Error(response?.message || "Failed to create data table");
298
+ return response.table ?? null;
299
+ } finally {
300
+ setLoading(false);
301
+ }
302
+ }, []),
303
+ loading
304
+ };
305
+ }
306
+ function useUpdateDataTable() {
307
+ const [loading, setLoading] = useState(false);
308
+ return {
309
+ updateDataTable: useCallback(async (id, input) => {
310
+ setLoading(true);
311
+ try {
312
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
313
+ mutation: UPDATE_DATA_TABLE,
314
+ variables: {
315
+ id,
316
+ input
317
+ }
318
+ })).data?.updateDataTable;
319
+ if (!response?.success) throw new Error(response?.message || "Failed to update data table");
320
+ return response.table ?? null;
321
+ } finally {
322
+ setLoading(false);
323
+ }
324
+ }, []),
325
+ loading
326
+ };
327
+ }
328
+ function useDeleteDataTable() {
329
+ const [loading, setLoading] = useState(false);
330
+ return {
331
+ deleteDataTable: useCallback(async (id) => {
332
+ setLoading(true);
333
+ try {
334
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
335
+ mutation: DELETE_DATA_TABLE,
336
+ variables: { id }
337
+ })).data?.deleteDataTable;
338
+ if (!response?.success) throw new Error(response?.message || "Failed to delete data table");
339
+ return true;
340
+ } finally {
341
+ setLoading(false);
342
+ }
343
+ }, []),
344
+ loading
345
+ };
346
+ }
347
+ function useInsertDataTableRow() {
348
+ const [loading, setLoading] = useState(false);
349
+ return {
350
+ insertDataTableRow: useCallback(async (tableId, data) => {
351
+ setLoading(true);
352
+ try {
353
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
354
+ mutation: INSERT_DATA_TABLE_ROW,
355
+ variables: {
356
+ tableId,
357
+ data
358
+ }
359
+ })).data?.insertDataTableRow;
360
+ if (!response?.success) throw new Error(response?.message || "Failed to insert row");
361
+ return response.row ? toRow(response.row) : null;
362
+ } finally {
363
+ setLoading(false);
364
+ }
365
+ }, []),
366
+ loading
367
+ };
368
+ }
369
+ function useUpdateDataTableRow() {
370
+ const [loading, setLoading] = useState(false);
371
+ return {
372
+ updateDataTableRow: useCallback(async (id, data) => {
373
+ setLoading(true);
374
+ try {
375
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
376
+ mutation: UPDATE_DATA_TABLE_ROW,
377
+ variables: {
378
+ id,
379
+ data
380
+ }
381
+ })).data?.updateDataTableRow;
382
+ if (!response?.success) throw new Error(response?.message || "Failed to update row");
383
+ return response.row ? toRow(response.row) : null;
384
+ } finally {
385
+ setLoading(false);
386
+ }
387
+ }, []),
388
+ loading
389
+ };
390
+ }
391
+ function useDeleteDataTableRows() {
392
+ const [loading, setLoading] = useState(false);
393
+ return {
394
+ deleteDataTableRows: useCallback(async (tableId, ids) => {
395
+ setLoading(true);
396
+ try {
397
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
398
+ mutation: DELETE_DATA_TABLE_ROWS,
399
+ variables: {
400
+ tableId,
401
+ ids
402
+ }
403
+ })).data?.deleteDataTableRows;
404
+ if (!response?.success) throw new Error(response?.message || "Failed to delete rows");
405
+ return response.affected;
406
+ } finally {
407
+ setLoading(false);
408
+ }
409
+ }, []),
410
+ loading
411
+ };
412
+ }
413
+
414
+ // src/modules/data-tables/utils/csv.ts
415
+ /** Parse RFC 4180-style CSV while preserving quoted commas and line breaks. */
416
+ function parseCsv(text) {
417
+ const rows = [];
418
+ let row = [];
419
+ let cell = "";
420
+ let quoted = false;
421
+ for (let index = 0; index < text.length; index += 1) {
422
+ const character = text[index];
423
+ const next = text[index + 1];
424
+ if (quoted) {
425
+ if (character === "\"" && next === "\"") {
426
+ cell += "\"";
427
+ index += 1;
428
+ } else if (character === "\"") quoted = false;
429
+ else cell += character;
430
+ continue;
431
+ }
432
+ if (character === "\"" && cell.length === 0) quoted = true;
433
+ else if (character === ",") {
434
+ row.push(cell);
435
+ cell = "";
436
+ } else if (character === "\n" || character === "\r") {
437
+ if (character === "\r" && next === "\n") index += 1;
438
+ row.push(cell);
439
+ cell = "";
440
+ if (row.some((value) => value.length > 0)) rows.push(row);
441
+ row = [];
442
+ } else cell += character;
443
+ }
444
+ if (quoted) throw new Error("CSV contains an unterminated quoted value");
445
+ if (cell.length > 0 || row.length > 0) {
446
+ row.push(cell);
447
+ if (row.some((value) => value.length > 0)) rows.push(row);
448
+ }
449
+ return rows;
450
+ }
451
+ function parseValue(value, type) {
452
+ if (value === "") return type === "STRING" ? "" : null;
453
+ switch (type) {
454
+ case "NUMBER": {
455
+ const parsed = Number(value);
456
+ if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got "${value}"`);
457
+ return parsed;
458
+ }
459
+ case "BOOLEAN": {
460
+ const normalized = value.trim().toLowerCase();
461
+ if (normalized === "true") return true;
462
+ if (normalized === "false") return false;
463
+ throw new Error(`Expected true or false, got "${value}"`);
464
+ }
465
+ case "JSON": try {
466
+ return JSON.parse(value);
467
+ } catch {
468
+ throw new Error(`Expected valid JSON, got "${value}"`);
469
+ }
470
+ case "DATE":
471
+ case "STRING": return value;
472
+ }
473
+ }
474
+ /** Convert a CSV whose first row contains column keys into validated row data. */
475
+ function csvToRows(text, columns) {
476
+ const parsed = parseCsv(text);
477
+ if (parsed.length < 2) return [];
478
+ const headers = parsed[0].map((header) => header.trim());
479
+ const columnByKey = new Map(columns.map((column) => [column.key, column]));
480
+ const seen = /* @__PURE__ */ new Set();
481
+ for (const header of headers) {
482
+ if (!header || seen.has(header)) throw new Error("CSV must have unique, non-empty column headers");
483
+ if (!columnByKey.has(header)) throw new Error(`CSV contains an unknown column "${header}"`);
484
+ seen.add(header);
485
+ }
486
+ return parsed.slice(1).map((values, rowIndex) => {
487
+ const row = {};
488
+ headers.forEach((header, columnIndex) => {
489
+ const column = columnByKey.get(header);
490
+ if (!column) return;
491
+ const rawValue = values[columnIndex] ?? "";
492
+ try {
493
+ row[header] = parseValue(rawValue, column.type);
494
+ } catch (error) {
495
+ const message = error instanceof Error ? error.message : "Invalid value";
496
+ throw new Error(`Row ${rowIndex + 2}, ${column.label}: ${message}`);
497
+ }
498
+ });
499
+ return row;
500
+ });
501
+ }
502
+ function csvCell(value) {
503
+ if (value === null || value === void 0) return "";
504
+ const text = typeof value === "object" ? JSON.stringify(value) : String(value);
505
+ return /[",\r\n]/.test(text) ? `"${text.replaceAll("\"", "\"\"")}"` : text;
506
+ }
507
+ /** Serialize table rows with a stable header matching the table schema. */
508
+ function rowsToCsv(columns, rows) {
509
+ return [columns.map((column) => csvCell(column.key)).join(","), ...rows.map((row) => columns.map((column) => csvCell(row.data[column.key])).join(","))].join("\r\n");
510
+ }
511
+
512
+ // src/modules/data-tables/utils/paging.ts
513
+ /** Page sizes offered in the rows pager; the first entry is the default. */
514
+ const ROW_PAGE_SIZES = [
515
+ 50,
516
+ 100,
517
+ 250
518
+ ];
519
+ const DEFAULT_ROW_PAGE_SIZE = ROW_PAGE_SIZES[0];
520
+ /** Number of pages needed for `total` rows; an empty table still has one page. */
521
+ function pageCount(total, pageSize) {
522
+ if (pageSize <= 0) return 1;
523
+ return Math.max(1, Math.ceil(total / pageSize));
524
+ }
525
+ /**
526
+ * Keep a zero-based page index inside the table. Needed after deleting rows,
527
+ * when the page being viewed may no longer exist.
528
+ */
529
+ function clampPage(page, total, pageSize) {
530
+ return Math.min(Math.max(page, 0), pageCount(total, pageSize) - 1);
531
+ }
532
+ /** One-based, inclusive `from`–`to` range of the rows shown on a page. */
533
+ function pageRange(page, pageSize, shown) {
534
+ if (shown <= 0) return {
535
+ from: 0,
536
+ to: 0
537
+ };
538
+ const from = page * pageSize + 1;
539
+ return {
540
+ from,
541
+ to: from + shown - 1
542
+ };
543
+ }
544
+
545
+ // src/modules/data-tables/pages/DataTablesPage.tsx
546
+ var DataTablesPage_exports = /* @__PURE__ */ __exportAll({ DataTablesPage: () => DataTablesPage });
547
+ const COLUMN_TYPES = [
548
+ "STRING",
549
+ "NUMBER",
550
+ "BOOLEAN",
551
+ "DATE",
552
+ "JSON"
553
+ ];
554
+ function emptyColumn(index) {
555
+ return {
556
+ key: `column_${index}`,
557
+ label: `Column ${index}`,
558
+ type: "STRING",
559
+ required: false
560
+ };
561
+ }
562
+ function formatBytes(bytes) {
563
+ if (bytes < 1024) return `${bytes} B`;
564
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
565
+ return `${(bytes / 1048576).toFixed(1)} MB`;
566
+ }
567
+ function formatValue(value) {
568
+ if (value === null || value === void 0) return "—";
569
+ if (typeof value === "object") return JSON.stringify(value);
570
+ return String(value);
571
+ }
572
+ function columnTypeLabel(type) {
573
+ return type === "JSON" ? "JSON" : type[0] + type.slice(1).toLowerCase();
574
+ }
575
+ function asFormValue(value, type) {
576
+ if (value === null || value === void 0) return "";
577
+ if (type === "JSON") return JSON.stringify(value, null, 2);
578
+ return String(value);
579
+ }
580
+ function parseFormValue(value, type) {
581
+ if (type === "STRING" || type === "DATE") return value;
582
+ if (value.trim() === "") return null;
583
+ if (type === "NUMBER") {
584
+ const number = Number(value);
585
+ if (!Number.isFinite(number)) throw new Error(`"${value}" is not a number`);
586
+ return number;
587
+ }
588
+ if (type === "BOOLEAN") {
589
+ if (value === "true") return true;
590
+ if (value === "false") return false;
591
+ throw new Error("Boolean values must be true or false");
592
+ }
593
+ try {
594
+ return JSON.parse(value);
595
+ } catch {
596
+ throw new Error("JSON values must contain valid JSON");
597
+ }
598
+ }
599
+ function TableFormDialog({ open, table, onOpenChange, onSaved }) {
600
+ const tr = useTr();
601
+ const { createDataTable, loading: creating } = useCreateDataTable();
602
+ const { updateDataTable, loading: updating } = useUpdateDataTable();
603
+ const [name, setName] = useState("");
604
+ const [description, setDescription] = useState("");
605
+ const [columns, setColumns] = useState([]);
606
+ const editing = table !== null;
607
+ const saving = creating || updating;
608
+ useEffect(() => {
609
+ if (!open) return;
610
+ setName(table?.name ?? "");
611
+ setDescription(table?.description ?? "");
612
+ setColumns(table?.columns.map((column) => ({
613
+ key: column.key,
614
+ label: column.label,
615
+ type: column.type,
616
+ required: column.required,
617
+ ...column.defaultValue !== void 0 ? { defaultValue: column.defaultValue } : {}
618
+ })) ?? [emptyColumn(1)]);
619
+ }, [open, table]);
620
+ const updateColumn = useCallback((index, patch) => {
621
+ setColumns((current) => current.map((column, columnIndex) => columnIndex === index ? {
622
+ ...column,
623
+ ...patch
624
+ } : column));
625
+ }, []);
626
+ const handleSubmit = useCallback(async () => {
627
+ const trimmedName = name.trim();
628
+ if (!trimmedName) {
629
+ toast.error(tr("fluidgrids.dataTables.nameRequired", "Table name is required"));
630
+ return;
631
+ }
632
+ if (columns.length === 0 || columns.some((column) => !column.key.trim() || !column.label.trim())) {
633
+ toast.error(tr("fluidgrids.dataTables.columnsRequired", "Add at least one column with a key and label"));
634
+ return;
635
+ }
636
+ const normalizedColumns = columns.map((column) => ({
637
+ ...column,
638
+ key: column.key.trim(),
639
+ label: column.label.trim()
640
+ }));
641
+ if (new Set(normalizedColumns.map((column) => column.key)).size !== normalizedColumns.length) {
642
+ toast.error(tr("fluidgrids.dataTables.uniqueKeys", "Column keys must be unique"));
643
+ return;
644
+ }
645
+ try {
646
+ const saved = editing ? await updateDataTable(table.id, {
647
+ name: trimmedName,
648
+ description: description.trim() || void 0,
649
+ columns: normalizedColumns
650
+ }) : await createDataTable({
651
+ name: trimmedName,
652
+ description: description.trim() || void 0,
653
+ columns: normalizedColumns
654
+ });
655
+ toast.success(editing ? tr("fluidgrids.dataTables.updated", "Data table updated") : tr("fluidgrids.dataTables.created", "Data table created"));
656
+ onSaved(saved);
657
+ onOpenChange(false);
658
+ } catch (error) {
659
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.saveFailed", "Failed to save data table")));
660
+ }
661
+ }, [
662
+ columns,
663
+ createDataTable,
664
+ description,
665
+ editing,
666
+ name,
667
+ onOpenChange,
668
+ onSaved,
669
+ table,
670
+ tr,
671
+ updateDataTable
672
+ ]);
673
+ return /* @__PURE__ */ jsx(Dialog, {
674
+ open,
675
+ onOpenChange,
676
+ children: /* @__PURE__ */ jsxs(DialogContent, {
677
+ className: "max-h-[90vh] max-w-3xl overflow-y-auto",
678
+ children: [
679
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editTable", "Edit data table") : tr("fluidgrids.dataTables.newTable", "New data table") }) }),
680
+ /* @__PURE__ */ jsxs("div", {
681
+ className: "space-y-5 py-2",
682
+ children: [/* @__PURE__ */ jsxs("div", {
683
+ className: "grid gap-4 sm:grid-cols-2",
684
+ children: [/* @__PURE__ */ jsxs("div", {
685
+ className: "space-y-1.5",
686
+ children: [/* @__PURE__ */ jsx(Label, {
687
+ htmlFor: "data-table-name",
688
+ children: tr("fluidgrids.dataTables.name", "Name")
689
+ }), /* @__PURE__ */ jsx(Input, {
690
+ id: "data-table-name",
691
+ value: name,
692
+ onChange: (event) => setName(event.target.value),
693
+ placeholder: "contacts",
694
+ autoFocus: true
695
+ })]
696
+ }), /* @__PURE__ */ jsxs("div", {
697
+ className: "space-y-1.5",
698
+ children: [/* @__PURE__ */ jsx(Label, {
699
+ htmlFor: "data-table-description",
700
+ children: tr("fluidgrids.dataTables.description", "Description")
701
+ }), /* @__PURE__ */ jsx(Input, {
702
+ id: "data-table-description",
703
+ value: description,
704
+ onChange: (event) => setDescription(event.target.value),
705
+ placeholder: tr("fluidgrids.dataTables.descriptionPlaceholder", "Optional")
706
+ })]
707
+ })]
708
+ }), /* @__PURE__ */ jsxs("div", {
709
+ className: "space-y-3",
710
+ children: [/* @__PURE__ */ jsxs("div", {
711
+ className: "flex items-center justify-between gap-3",
712
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
713
+ className: "text-sm font-semibold text-text-primary",
714
+ children: tr("fluidgrids.dataTables.columns", "Columns")
715
+ }), /* @__PURE__ */ jsx("p", {
716
+ className: "text-xs text-text-secondary",
717
+ children: tr("fluidgrids.dataTables.columnsHint", "Column keys are used by workflow nodes and must be unique.")
718
+ })] }), /* @__PURE__ */ jsxs(Button$1, {
719
+ type: "button",
720
+ variant: "outline",
721
+ size: "sm",
722
+ onClick: () => setColumns((current) => [...current, emptyColumn(current.length + 1)]),
723
+ children: [/* @__PURE__ */ jsx(Plus, {
724
+ className: "mr-1.5 size-4",
725
+ "aria-hidden": "true"
726
+ }), tr("fluidgrids.dataTables.addColumn", "Add column")]
727
+ })]
728
+ }), /* @__PURE__ */ jsx("div", {
729
+ className: "space-y-2",
730
+ children: columns.map((column, index) => /* @__PURE__ */ jsxs("div", {
731
+ className: "grid gap-2 rounded-md border border-border-secondary p-3 sm:grid-cols-[1fr_1fr_8rem_auto] sm:items-end",
732
+ children: [
733
+ /* @__PURE__ */ jsxs("div", {
734
+ className: "space-y-1",
735
+ children: [/* @__PURE__ */ jsx(Label, {
736
+ htmlFor: `data-table-column-key-${index}`,
737
+ children: tr("fluidgrids.dataTables.key", "Key")
738
+ }), /* @__PURE__ */ jsx(Input, {
739
+ id: `data-table-column-key-${index}`,
740
+ value: column.key,
741
+ onChange: (event) => updateColumn(index, { key: event.target.value })
742
+ })]
743
+ }),
744
+ /* @__PURE__ */ jsxs("div", {
745
+ className: "space-y-1",
746
+ children: [/* @__PURE__ */ jsx(Label, {
747
+ htmlFor: `data-table-column-label-${index}`,
748
+ children: tr("fluidgrids.dataTables.label", "Label")
749
+ }), /* @__PURE__ */ jsx(Input, {
750
+ id: `data-table-column-label-${index}`,
751
+ value: column.label,
752
+ onChange: (event) => updateColumn(index, { label: event.target.value })
753
+ })]
754
+ }),
755
+ /* @__PURE__ */ jsxs("div", {
756
+ className: "space-y-1",
757
+ children: [/* @__PURE__ */ jsx(Label, {
758
+ htmlFor: `data-table-column-type-${index}`,
759
+ children: tr("fluidgrids.dataTables.type", "Type")
760
+ }), /* @__PURE__ */ jsx("select", {
761
+ id: `data-table-column-type-${index}`,
762
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-text-primary",
763
+ value: column.type,
764
+ onChange: (event) => updateColumn(index, { type: event.target.value }),
765
+ children: COLUMN_TYPES.map((type) => /* @__PURE__ */ jsx("option", {
766
+ value: type,
767
+ children: columnTypeLabel(type)
768
+ }, type))
769
+ })]
770
+ }),
771
+ /* @__PURE__ */ jsxs("div", {
772
+ className: "flex items-center gap-2 pb-2 sm:flex-col sm:items-center sm:gap-1",
773
+ children: [/* @__PURE__ */ jsxs("label", {
774
+ className: "flex items-center gap-2 text-xs text-text-secondary",
775
+ children: [/* @__PURE__ */ jsx("input", {
776
+ type: "checkbox",
777
+ checked: column.required ?? false,
778
+ onChange: (event) => updateColumn(index, { required: event.target.checked })
779
+ }), tr("fluidgrids.dataTables.required", "Required")]
780
+ }), /* @__PURE__ */ jsx("button", {
781
+ type: "button",
782
+ className: "rounded p-1 text-text-secondary hover:bg-background-tertiary hover:text-text-primary disabled:opacity-40",
783
+ onClick: () => setColumns((current) => current.filter((_, columnIndex) => columnIndex !== index)),
784
+ disabled: columns.length === 1,
785
+ title: tr("fluidgrids.dataTables.removeColumn", "Remove column"),
786
+ "aria-label": tr("fluidgrids.dataTables.removeColumn", "Remove column"),
787
+ children: /* @__PURE__ */ jsx(Trash2, {
788
+ className: "size-4",
789
+ "aria-hidden": "true"
790
+ })
791
+ })]
792
+ })
793
+ ]
794
+ }, `${column.key}-${index}`))
795
+ })]
796
+ })]
797
+ }),
798
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
799
+ variant: "outline",
800
+ onClick: () => onOpenChange(false),
801
+ children: tr("fluidgrids.dataTables.cancel", "Cancel")
802
+ }), /* @__PURE__ */ jsx(Button$1, {
803
+ onClick: handleSubmit,
804
+ disabled: saving,
805
+ children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.save", "Save table")
806
+ })] })
807
+ ]
808
+ })
809
+ });
810
+ }
811
+ function RowFormDialog({ open, table, row, onOpenChange, onSaved }) {
812
+ const tr = useTr();
813
+ const { insertDataTableRow, loading: inserting } = useInsertDataTableRow();
814
+ const { updateDataTableRow, loading: updating } = useUpdateDataTableRow();
815
+ const [values, setValues] = useState({});
816
+ const editing = row !== null;
817
+ const saving = inserting || updating;
818
+ useEffect(() => {
819
+ if (!open || !table) return;
820
+ const next = {};
821
+ for (const column of table.columns) next[column.key] = asFormValue(row?.data[column.key], column.type);
822
+ setValues(next);
823
+ }, [
824
+ open,
825
+ row,
826
+ table
827
+ ]);
828
+ const handleSubmit = useCallback(async () => {
829
+ if (!table) return;
830
+ try {
831
+ const data = {};
832
+ for (const column of table.columns) {
833
+ const value = values[column.key] ?? "";
834
+ if (column.required && value.trim() === "") throw new Error(`${column.label} is required`);
835
+ data[column.key] = parseFormValue(value, column.type);
836
+ }
837
+ if (editing && row) await updateDataTableRow(row.id, data);
838
+ else await insertDataTableRow(table.id, data);
839
+ toast.success(editing ? tr("fluidgrids.dataTables.rowUpdated", "Row updated") : tr("fluidgrids.dataTables.rowCreated", "Row added"));
840
+ onSaved();
841
+ onOpenChange(false);
842
+ } catch (error) {
843
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.rowSaveFailed", "Failed to save row")));
844
+ }
845
+ }, [
846
+ editing,
847
+ insertDataTableRow,
848
+ onOpenChange,
849
+ onSaved,
850
+ row,
851
+ table,
852
+ tr,
853
+ updateDataTableRow,
854
+ values
855
+ ]);
856
+ return /* @__PURE__ */ jsx(Dialog, {
857
+ open,
858
+ onOpenChange,
859
+ children: /* @__PURE__ */ jsxs(DialogContent, {
860
+ className: "max-h-[90vh] max-w-2xl overflow-y-auto",
861
+ children: [
862
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editRow", "Edit row") : tr("fluidgrids.dataTables.addRow", "Add row") }) }),
863
+ table && /* @__PURE__ */ jsx("div", {
864
+ className: "space-y-4 py-2",
865
+ children: table.columns.map((column) => /* @__PURE__ */ jsxs("div", {
866
+ className: "space-y-1.5",
867
+ children: [/* @__PURE__ */ jsxs(Label, {
868
+ htmlFor: `data-table-row-${column.key}`,
869
+ children: [
870
+ column.label,
871
+ /* @__PURE__ */ jsxs("span", {
872
+ className: "ml-1 text-xs text-text-secondary",
873
+ children: [
874
+ "(",
875
+ column.key,
876
+ " · ",
877
+ columnTypeLabel(column.type),
878
+ ")"
879
+ ]
880
+ }),
881
+ column.required && /* @__PURE__ */ jsx("span", {
882
+ className: "ml-1 text-text-secondary",
883
+ children: "*"
884
+ })
885
+ ]
886
+ }), column.type === "JSON" ? /* @__PURE__ */ jsx(Textarea, {
887
+ id: `data-table-row-${column.key}`,
888
+ value: values[column.key] ?? "",
889
+ onChange: (event) => setValues((current) => ({
890
+ ...current,
891
+ [column.key]: event.target.value
892
+ })),
893
+ rows: 4,
894
+ placeholder: "{ }"
895
+ }) : /* @__PURE__ */ jsx(Input, {
896
+ id: `data-table-row-${column.key}`,
897
+ type: column.type === "NUMBER" ? "number" : column.type === "DATE" ? "datetime-local" : "text",
898
+ value: values[column.key] ?? "",
899
+ onChange: (event) => setValues((current) => ({
900
+ ...current,
901
+ [column.key]: event.target.value
902
+ }))
903
+ })]
904
+ }, column.key))
905
+ }),
906
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
907
+ variant: "outline",
908
+ onClick: () => onOpenChange(false),
909
+ children: tr("fluidgrids.dataTables.cancel", "Cancel")
910
+ }), /* @__PURE__ */ jsx(Button$1, {
911
+ onClick: handleSubmit,
912
+ disabled: saving || !table,
913
+ children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.saveRow", "Save row")
914
+ })] })
915
+ ]
916
+ })
917
+ });
918
+ }
919
+ function TableRowsPanel({ table, onTableChanged, onEditTable, onDeleteTable, deletingTable, onTagsChanged }) {
920
+ const tr = useTr();
921
+ const { allowed: canTag } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
922
+ const [page, setPage] = useState(0);
923
+ const [pageSize, setPageSize] = useState(DEFAULT_ROW_PAGE_SIZE);
924
+ const { rows, total, loading, error, refetch } = useDataTableRows(table.id, {
925
+ page,
926
+ pageSize
927
+ });
928
+ const { deleteDataTableRows, loading: deleting } = useDeleteDataTableRows();
929
+ const { insertDataTableRow } = useInsertDataTableRow();
930
+ const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
931
+ const [rowDialogOpen, setRowDialogOpen] = useState(false);
932
+ const [editingRow, setEditingRow] = useState(null);
933
+ const [exporting, setExporting] = useState(false);
934
+ const [importProgress, setImportProgress] = useState(null);
935
+ const fileInputRef = useRef(null);
936
+ const pages = pageCount(total, pageSize);
937
+ const range = pageRange(page, pageSize, rows.length);
938
+ useEffect(() => {
939
+ if (loading) return;
940
+ const clamped = clampPage(page, total, pageSize);
941
+ if (clamped !== page) setPage(clamped);
942
+ }, [
943
+ loading,
944
+ page,
945
+ pageSize,
946
+ total
947
+ ]);
948
+ useEffect(() => {
949
+ setSelectedIds((current) => {
950
+ const visible = new Set(rows.map((row) => row.id));
951
+ return new Set([...current].filter((id) => visible.has(id)));
952
+ });
953
+ }, [rows]);
954
+ const allVisibleSelected = rows.length > 0 && selectedIds.size === rows.length;
955
+ const toggleAll = useCallback(() => {
956
+ setSelectedIds(allVisibleSelected ? /* @__PURE__ */ new Set() : new Set(rows.map((row) => row.id)));
957
+ }, [allVisibleSelected, rows]);
958
+ const handleDeleteRows = useCallback(async () => {
959
+ if (selectedIds.size === 0) return;
960
+ if (!window.confirm(tr("fluidgrids.dataTables.confirmDeleteRows", `Delete ${selectedIds.size} selected row(s)?`))) return;
961
+ try {
962
+ const affected = await deleteDataTableRows(table.id, [...selectedIds]);
963
+ toast.success(tr("fluidgrids.dataTables.rowsDeleted", `${affected} row(s) deleted`));
964
+ setSelectedIds(/* @__PURE__ */ new Set());
965
+ await refetch();
966
+ onTableChanged();
967
+ } catch (error) {
968
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.deleteRowsFailed", "Failed to delete rows")));
969
+ }
970
+ }, [
971
+ deleteDataTableRows,
972
+ onTableChanged,
973
+ refetch,
974
+ selectedIds,
975
+ table.id,
976
+ tr
977
+ ]);
978
+ const handleExport = useCallback(async () => {
979
+ setExporting(true);
980
+ try {
981
+ const allRows = await fetchAllDataTableRows(table.id);
982
+ const csv = rowsToCsv(table.columns, allRows);
983
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
984
+ const url = URL.createObjectURL(blob);
985
+ const anchor = document.createElement("a");
986
+ anchor.href = url;
987
+ anchor.download = `${table.name}.csv`;
988
+ anchor.click();
989
+ URL.revokeObjectURL(url);
990
+ toast.success(tr("fluidgrids.dataTables.exported", `${allRows.length} row(s) exported to CSV`));
991
+ } catch (error) {
992
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.exportFailed", "Failed to export CSV")));
993
+ } finally {
994
+ setExporting(false);
995
+ }
996
+ }, [
997
+ table.columns,
998
+ table.id,
999
+ table.name,
1000
+ tr
1001
+ ]);
1002
+ const handleImport = useCallback(async (event) => {
1003
+ const file = event.target.files?.[0];
1004
+ event.target.value = "";
1005
+ if (!file) return;
1006
+ let inserted = 0;
1007
+ try {
1008
+ const importedRows = csvToRows(await file.text(), table.columns);
1009
+ setImportProgress({
1010
+ done: 0,
1011
+ total: importedRows.length
1012
+ });
1013
+ for (const data of importedRows) {
1014
+ await insertDataTableRow(table.id, data);
1015
+ inserted += 1;
1016
+ setImportProgress({
1017
+ done: inserted,
1018
+ total: importedRows.length
1019
+ });
1020
+ }
1021
+ toast.success(tr("fluidgrids.dataTables.imported", `${inserted} row(s) imported`));
1022
+ } catch (error) {
1023
+ const reason = describeOperationError(error, tr("fluidgrids.dataTables.importFailed", "Failed to import CSV"));
1024
+ toast.error(inserted > 0 ? tr("fluidgrids.dataTables.importPartial", `Import stopped after ${inserted} row(s): ${reason}`) : reason);
1025
+ } finally {
1026
+ setImportProgress(null);
1027
+ setPage(0);
1028
+ await refetch();
1029
+ onTableChanged();
1030
+ }
1031
+ }, [
1032
+ insertDataTableRow,
1033
+ onTableChanged,
1034
+ refetch,
1035
+ table.columns,
1036
+ table.id,
1037
+ tr
1038
+ ]);
1039
+ const openNewRow = useCallback(() => {
1040
+ setEditingRow(null);
1041
+ setRowDialogOpen(true);
1042
+ }, []);
1043
+ const openEditRow = useCallback((row) => {
1044
+ setEditingRow(row);
1045
+ setRowDialogOpen(true);
1046
+ }, []);
1047
+ return /* @__PURE__ */ jsxs(Card, {
1048
+ className: "min-w-0 border-border-secondary",
1049
+ children: [
1050
+ /* @__PURE__ */ jsxs(CardHeader, {
1051
+ className: "gap-4 border-b border-border-secondary p-4 sm:p-5",
1052
+ children: [/* @__PURE__ */ jsxs("div", {
1053
+ className: "flex flex-wrap items-start justify-between gap-3",
1054
+ children: [/* @__PURE__ */ jsxs("div", {
1055
+ className: "min-w-0 flex-1 basis-64",
1056
+ children: [
1057
+ /* @__PURE__ */ jsxs(CardTitle, {
1058
+ className: "flex items-center gap-2 text-lg",
1059
+ children: [/* @__PURE__ */ jsx(Table2, {
1060
+ className: "size-5 shrink-0 text-text-secondary",
1061
+ "aria-hidden": "true"
1062
+ }), /* @__PURE__ */ jsx("span", {
1063
+ className: "truncate",
1064
+ children: table.name
1065
+ })]
1066
+ }),
1067
+ /* @__PURE__ */ jsx(CardDescription, {
1068
+ className: "mt-1",
1069
+ children: table.description || tr("fluidgrids.dataTables.noDescription", "No description")
1070
+ }),
1071
+ /* @__PURE__ */ jsx("div", {
1072
+ className: "mt-3",
1073
+ "data-testid": "data-table-tags",
1074
+ children: /* @__PURE__ */ jsx(EntityTagPicker, {
1075
+ entityType: "FLUIDGRIDS_DATA_TABLE",
1076
+ entityId: table.id,
1077
+ readOnly: !canTag,
1078
+ onChange: onTagsChanged
1079
+ })
1080
+ })
1081
+ ]
1082
+ }), /* @__PURE__ */ jsxs("div", {
1083
+ className: "flex shrink-0 items-center gap-2",
1084
+ children: [/* @__PURE__ */ jsx(SdkPermissionButton, {
1085
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_UPDATE,
1086
+ deniedTooltip: tr("fluidgrids.dataTables.editDeniedTooltip", "You don't have permission to edit data tables"),
1087
+ children: /* @__PURE__ */ jsxs(Button$1, {
1088
+ variant: "outline",
1089
+ size: "sm",
1090
+ className: "whitespace-nowrap",
1091
+ onClick: onEditTable,
1092
+ children: [/* @__PURE__ */ jsx(Pencil, {
1093
+ className: "mr-1.5 size-4",
1094
+ "aria-hidden": "true"
1095
+ }), tr("fluidgrids.dataTables.editTable", "Edit table")]
1096
+ })
1097
+ }), /* @__PURE__ */ jsx(SdkPermissionButton, {
1098
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_DELETE,
1099
+ deniedTooltip: tr("fluidgrids.dataTables.deleteDeniedTooltip", "You don't have permission to delete data tables"),
1100
+ children: /* @__PURE__ */ jsxs(Button$1, {
1101
+ variant: "outline",
1102
+ size: "sm",
1103
+ className: "whitespace-nowrap",
1104
+ onClick: onDeleteTable,
1105
+ disabled: deletingTable,
1106
+ children: [/* @__PURE__ */ jsx(Trash2, {
1107
+ className: "mr-1.5 size-4",
1108
+ "aria-hidden": "true"
1109
+ }), deletingTable ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteTable", "Delete table")]
1110
+ })
1111
+ })]
1112
+ })]
1113
+ }), /* @__PURE__ */ jsxs("div", {
1114
+ className: "flex flex-wrap items-center justify-between gap-3",
1115
+ children: [/* @__PURE__ */ jsxs("div", {
1116
+ className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary",
1117
+ children: [
1118
+ /* @__PURE__ */ jsxs(Badge, {
1119
+ variant: "outline",
1120
+ children: [
1121
+ total,
1122
+ " ",
1123
+ tr("fluidgrids.dataTables.rows", "rows")
1124
+ ]
1125
+ }),
1126
+ /* @__PURE__ */ jsxs(Badge, {
1127
+ variant: "outline",
1128
+ children: [
1129
+ table.columns.length,
1130
+ " ",
1131
+ tr("fluidgrids.dataTables.columns", "columns")
1132
+ ]
1133
+ }),
1134
+ /* @__PURE__ */ jsx(Badge, {
1135
+ variant: "outline",
1136
+ children: formatBytes(table.sizeBytes)
1137
+ }),
1138
+ selectedIds.size > 0 && /* @__PURE__ */ jsx(SdkPermissionButton, {
1139
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE,
1140
+ deniedTooltip: tr("fluidgrids.dataTables.deleteRowsDeniedTooltip", "You don't have permission to delete rows"),
1141
+ children: /* @__PURE__ */ jsxs(Button$1, {
1142
+ variant: "outline",
1143
+ size: "sm",
1144
+ className: "whitespace-nowrap",
1145
+ onClick: handleDeleteRows,
1146
+ disabled: deleting,
1147
+ children: [/* @__PURE__ */ jsx(Trash2, {
1148
+ className: "mr-1.5 size-4",
1149
+ "aria-hidden": "true"
1150
+ }), deleting ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteSelected", `Delete ${selectedIds.size} selected`)]
1151
+ })
1152
+ })
1153
+ ]
1154
+ }), /* @__PURE__ */ jsxs("div", {
1155
+ className: "flex flex-wrap items-center gap-2",
1156
+ children: [
1157
+ /* @__PURE__ */ jsx(SdkPermissionButton, {
1158
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
1159
+ deniedTooltip: tr("fluidgrids.dataTables.addRowDeniedTooltip", "You don't have permission to add rows"),
1160
+ children: /* @__PURE__ */ jsxs(Button$1, {
1161
+ size: "sm",
1162
+ className: "whitespace-nowrap",
1163
+ onClick: openNewRow,
1164
+ disabled: importProgress !== null,
1165
+ children: [/* @__PURE__ */ jsx(Plus, {
1166
+ className: "mr-1.5 size-4",
1167
+ "aria-hidden": "true"
1168
+ }), tr("fluidgrids.dataTables.addRow", "Add row")]
1169
+ })
1170
+ }),
1171
+ /* @__PURE__ */ jsx(SdkPermissionButton, {
1172
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
1173
+ deniedTooltip: tr("fluidgrids.dataTables.importDeniedTooltip", "You don't have permission to import rows"),
1174
+ children: /* @__PURE__ */ jsxs(Button$1, {
1175
+ variant: "outline",
1176
+ size: "sm",
1177
+ className: "whitespace-nowrap",
1178
+ onClick: () => fileInputRef.current?.click(),
1179
+ disabled: importProgress !== null,
1180
+ children: [/* @__PURE__ */ jsx(ArrowUpFromLine, {
1181
+ className: "mr-1.5 size-4",
1182
+ "aria-hidden": "true"
1183
+ }), importProgress ? tr("fluidgrids.dataTables.importing", `Importing ${importProgress.done} / ${importProgress.total}…`) : tr("fluidgrids.dataTables.import", "Import CSV")]
1184
+ })
1185
+ }),
1186
+ /* @__PURE__ */ jsx("input", {
1187
+ ref: fileInputRef,
1188
+ type: "file",
1189
+ accept: ".csv,text/csv",
1190
+ className: "hidden",
1191
+ onChange: handleImport
1192
+ }),
1193
+ /* @__PURE__ */ jsxs(Button$1, {
1194
+ variant: "outline",
1195
+ size: "sm",
1196
+ className: "whitespace-nowrap",
1197
+ onClick: handleExport,
1198
+ disabled: total === 0 || exporting,
1199
+ children: [/* @__PURE__ */ jsx(ArrowDownToLine, {
1200
+ className: "mr-1.5 size-4",
1201
+ "aria-hidden": "true"
1202
+ }), exporting ? tr("fluidgrids.dataTables.exporting", "Exporting…") : tr("fluidgrids.dataTables.export", "Export CSV")]
1203
+ })
1204
+ ]
1205
+ })]
1206
+ })]
1207
+ }),
1208
+ /* @__PURE__ */ jsxs(CardContent, {
1209
+ className: "p-0",
1210
+ children: [
1211
+ error && /* @__PURE__ */ jsx("div", {
1212
+ className: "m-4 rounded-md border border-border-secondary bg-background-secondary p-3 text-sm text-text-secondary",
1213
+ children: error.message
1214
+ }),
1215
+ /* @__PURE__ */ jsx("div", {
1216
+ className: "overflow-x-auto",
1217
+ children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
1218
+ /* @__PURE__ */ jsx(TableHead, {
1219
+ className: "w-10",
1220
+ children: /* @__PURE__ */ jsx("input", {
1221
+ type: "checkbox",
1222
+ checked: allVisibleSelected,
1223
+ onChange: toggleAll,
1224
+ "aria-label": tr("fluidgrids.dataTables.selectAll", "Select all rows on this page")
1225
+ })
1226
+ }),
1227
+ /* @__PURE__ */ jsx(TableHead, { children: tr("fluidgrids.dataTables.rowId", "Row") }),
1228
+ table.columns.map((column) => /* @__PURE__ */ jsx(TableHead, { children: column.label }, column.key)),
1229
+ /* @__PURE__ */ jsx(TableHead, {
1230
+ className: "text-right",
1231
+ children: tr("fluidgrids.dataTables.actions", "Actions")
1232
+ })
1233
+ ] }) }), /* @__PURE__ */ jsxs(TableBody, { children: [
1234
+ loading && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
1235
+ colSpan: table.columns.length + 3,
1236
+ className: "py-10 text-center text-text-secondary",
1237
+ children: tr("fluidgrids.dataTables.loadingRows", "Loading rows…")
1238
+ }) }),
1239
+ !loading && rows.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
1240
+ colSpan: table.columns.length + 3,
1241
+ className: "py-10 text-center text-text-secondary",
1242
+ children: tr("fluidgrids.dataTables.emptyRows", "No rows yet. Add a row or import a CSV to get started.")
1243
+ }) }),
1244
+ !loading && rows.map((row) => /* @__PURE__ */ jsxs(TableRow, { children: [
1245
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("input", {
1246
+ type: "checkbox",
1247
+ checked: selectedIds.has(row.id),
1248
+ onChange: () => setSelectedIds((current) => {
1249
+ const next = new Set(current);
1250
+ if (next.has(row.id)) next.delete(row.id);
1251
+ else next.add(row.id);
1252
+ return next;
1253
+ }),
1254
+ "aria-label": tr("fluidgrids.dataTables.selectRow", "Select row")
1255
+ }) }),
1256
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("code", {
1257
+ className: "text-xs text-text-secondary",
1258
+ children: row.id.slice(0, 8)
1259
+ }) }),
1260
+ table.columns.map((column) => /* @__PURE__ */ jsx(TableCell, {
1261
+ className: "max-w-[16rem] truncate text-sm",
1262
+ title: formatValue(row.data[column.key]),
1263
+ children: formatValue(row.data[column.key])
1264
+ }, column.key)),
1265
+ /* @__PURE__ */ jsx(TableCell, {
1266
+ className: "text-right",
1267
+ children: /* @__PURE__ */ jsx(SdkPermissionButton, {
1268
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_UPDATE,
1269
+ deniedTooltip: tr("fluidgrids.dataTables.editRowDeniedTooltip", "You don't have permission to edit rows"),
1270
+ children: /* @__PURE__ */ jsx(Button$1, {
1271
+ variant: "ghost",
1272
+ size: "icon",
1273
+ onClick: () => openEditRow(row),
1274
+ title: tr("fluidgrids.dataTables.editRow", "Edit row"),
1275
+ "aria-label": tr("fluidgrids.dataTables.editRow", "Edit row"),
1276
+ children: /* @__PURE__ */ jsx(Pencil, {
1277
+ className: "size-4",
1278
+ "aria-hidden": "true"
1279
+ })
1280
+ })
1281
+ })
1282
+ })
1283
+ ] }, row.id))
1284
+ ] })] })
1285
+ }),
1286
+ total > 0 && /* @__PURE__ */ jsxs("div", {
1287
+ className: "flex flex-wrap items-center justify-between gap-3 border-t border-border-secondary px-4 py-3 text-xs text-text-secondary",
1288
+ children: [/* @__PURE__ */ jsx("span", {
1289
+ "aria-live": "polite",
1290
+ children: tr("fluidgrids.dataTables.showingRange", `Showing ${range.from}–${range.to} of ${total}`)
1291
+ }), /* @__PURE__ */ jsxs("div", {
1292
+ className: "flex flex-wrap items-center gap-3",
1293
+ children: [
1294
+ /* @__PURE__ */ jsxs("label", {
1295
+ className: "flex items-center gap-2",
1296
+ children: [/* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.rowsPerPage", "Rows per page") }), /* @__PURE__ */ jsx("select", {
1297
+ className: "h-8 rounded-md border border-input bg-background px-2 text-xs text-text-primary",
1298
+ value: pageSize,
1299
+ onChange: (event) => {
1300
+ setPageSize(Number(event.target.value));
1301
+ setPage(0);
1302
+ },
1303
+ children: ROW_PAGE_SIZES.map((size) => /* @__PURE__ */ jsx("option", {
1304
+ value: size,
1305
+ children: size
1306
+ }, size))
1307
+ })]
1308
+ }),
1309
+ /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.pageOf", `Page ${page + 1} of ${pages}`) }),
1310
+ /* @__PURE__ */ jsxs("div", {
1311
+ className: "flex items-center gap-1",
1312
+ children: [/* @__PURE__ */ jsx(Button$1, {
1313
+ variant: "outline",
1314
+ size: "icon",
1315
+ className: "size-8",
1316
+ onClick: () => setPage((current) => Math.max(current - 1, 0)),
1317
+ disabled: page === 0 || loading,
1318
+ title: tr("fluidgrids.dataTables.prevPage", "Previous page"),
1319
+ "aria-label": tr("fluidgrids.dataTables.prevPage", "Previous page"),
1320
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
1321
+ className: "size-4",
1322
+ "aria-hidden": "true"
1323
+ })
1324
+ }), /* @__PURE__ */ jsx(Button$1, {
1325
+ variant: "outline",
1326
+ size: "icon",
1327
+ className: "size-8",
1328
+ onClick: () => setPage((current) => Math.min(current + 1, pages - 1)),
1329
+ disabled: page >= pages - 1 || loading,
1330
+ title: tr("fluidgrids.dataTables.nextPage", "Next page"),
1331
+ "aria-label": tr("fluidgrids.dataTables.nextPage", "Next page"),
1332
+ children: /* @__PURE__ */ jsx(ChevronRight, {
1333
+ className: "size-4",
1334
+ "aria-hidden": "true"
1335
+ })
1336
+ })]
1337
+ })
1338
+ ]
1339
+ })]
1340
+ })
1341
+ ]
1342
+ }),
1343
+ /* @__PURE__ */ jsx(RowFormDialog, {
1344
+ open: rowDialogOpen,
1345
+ table,
1346
+ row: editingRow,
1347
+ onOpenChange: setRowDialogOpen,
1348
+ onSaved: async () => {
1349
+ await refetch();
1350
+ onTableChanged();
1351
+ }
1352
+ })
1353
+ ]
1354
+ });
1355
+ }
1356
+ function DataTablesPage() {
1357
+ const tr = useTr();
1358
+ const navigate = useNavigate();
1359
+ const { allowed: canCreateTable } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE);
1360
+ const [searchInput, setSearchInput] = useState("");
1361
+ const [search, setSearch] = useState("");
1362
+ const [tagFilter, setTagFilter] = useState("");
1363
+ const { tables, total, loading, error, refetch } = useDataTables(search, tagFilter);
1364
+ const { tagsById, refetch: refetchTags } = useDataTableTagOverlay();
1365
+ const { deleteDataTable, loading: deleting } = useDeleteDataTable();
1366
+ const [selectedId, setSelectedId] = useState(null);
1367
+ const [tableDialogOpen, setTableDialogOpen] = useState(false);
1368
+ const [editingTable, setEditingTable] = useState(null);
1369
+ const selectedTable = useMemo(() => tables.find((table) => table.id === selectedId) ?? null, [selectedId, tables]);
1370
+ useEffect(() => {
1371
+ if (tables.length === 0) {
1372
+ setSelectedId(null);
1373
+ return;
1374
+ }
1375
+ if (!selectedId || !tables.some((table) => table.id === selectedId)) setSelectedId(tables[0].id);
1376
+ }, [selectedId, tables]);
1377
+ const refresh = useCallback(async () => {
1378
+ await refetch();
1379
+ }, [refetch]);
1380
+ const handleTagsChanged = useCallback(() => {
1381
+ refetchTags();
1382
+ if (tagFilter) refetch();
1383
+ }, [
1384
+ refetch,
1385
+ refetchTags,
1386
+ tagFilter
1387
+ ]);
1388
+ const handleDeleteTable = useCallback(async () => {
1389
+ if (!selectedTable) return;
1390
+ if (!window.confirm(tr("fluidgrids.dataTables.confirmDeleteTable", `Delete table "${selectedTable.name}" and all of its rows?`))) return;
1391
+ try {
1392
+ await deleteDataTable(selectedTable.id);
1393
+ toast.success(tr("fluidgrids.dataTables.deleted", "Data table deleted"));
1394
+ setSelectedId(null);
1395
+ await refetch();
1396
+ } catch (error) {
1397
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.deleteFailed", "Failed to delete data table")));
1398
+ }
1399
+ }, [
1400
+ deleteDataTable,
1401
+ refetch,
1402
+ selectedTable,
1403
+ tr
1404
+ ]);
1405
+ const handleTableSaved = useCallback((table) => {
1406
+ if (table) setSelectedId(table.id);
1407
+ refetch();
1408
+ }, [refetch]);
1409
+ const submitSearch = useCallback((event) => {
1410
+ event.preventDefault();
1411
+ setSearch(searchInput);
1412
+ }, [searchInput]);
1413
+ return /* @__PURE__ */ jsxs("div", {
1414
+ className: "min-h-screen bg-background-primary p-4 pb-24 sm:p-6 sm:pb-24",
1415
+ children: [/* @__PURE__ */ jsxs("div", {
1416
+ className: "mx-auto flex max-w-[1500px] flex-col gap-5",
1417
+ children: [
1418
+ /* @__PURE__ */ jsxs("div", {
1419
+ className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
1420
+ "data-tour": "fluidgrids-data-tables-header",
1421
+ children: [/* @__PURE__ */ jsxs("div", { children: [
1422
+ /* @__PURE__ */ jsxs("div", {
1423
+ className: "mb-2 flex items-center gap-2 text-sm text-text-secondary",
1424
+ children: [/* @__PURE__ */ jsx(Database, {
1425
+ className: "size-4",
1426
+ "aria-hidden": "true"
1427
+ }), tr("fluidgrids.dataTables.workflowData", "Workflow data")]
1428
+ }),
1429
+ /* @__PURE__ */ jsx("h1", {
1430
+ className: "text-2xl font-bold text-text-primary sm:text-3xl",
1431
+ children: tr("fluidgrids.dataTables.pageTitle", "Data Tables")
1432
+ }),
1433
+ /* @__PURE__ */ jsx(PagePurpose, {
1434
+ icon: /* @__PURE__ */ jsx(Database, {
1435
+ className: "size-4",
1436
+ "aria-hidden": "true"
1437
+ }),
1438
+ children: tr("fluidgrids.dataTables.pageSubtitle", "Use Data Tables as lightweight persistent storage for workflow state, lookups, and records shared across runs.")
1439
+ }),
1440
+ /* @__PURE__ */ jsx(FluidGridsDocsLink, {
1441
+ topic: "dataTables",
1442
+ className: "mt-1"
1443
+ })
1444
+ ] }), /* @__PURE__ */ jsxs("div", {
1445
+ className: "flex flex-wrap gap-2",
1446
+ children: [/* @__PURE__ */ jsxs(Button$1, {
1447
+ variant: "outline",
1448
+ onClick: () => navigate("../list"),
1449
+ children: [/* @__PURE__ */ jsx(ArrowLeft, {
1450
+ className: "mr-2 size-4",
1451
+ "aria-hidden": "true"
1452
+ }), tr("fluidgrids.dataTables.backToWorkflows", "Back to Workflows")]
1453
+ }), /* @__PURE__ */ jsx(SdkPermissionButton, {
1454
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE,
1455
+ deniedTooltip: tr("fluidgrids.dataTables.createDeniedTooltip", "You don't have permission to create data tables"),
1456
+ children: /* @__PURE__ */ jsxs(Button$1, {
1457
+ onClick: () => {
1458
+ setEditingTable(null);
1459
+ setTableDialogOpen(true);
1460
+ },
1461
+ "data-tour": "fluidgrids-data-tables-create",
1462
+ children: [/* @__PURE__ */ jsx(Plus, {
1463
+ className: "mr-2 size-4",
1464
+ "aria-hidden": "true"
1465
+ }), tr("fluidgrids.dataTables.newTable", "New data table")]
1466
+ })
1467
+ })]
1468
+ })]
1469
+ }),
1470
+ /* @__PURE__ */ jsx(NextSteps, {
1471
+ storageKey: "data-tables",
1472
+ title: tr("fluidgrids.dataTables.nextStepsTitle", "How to use Data Tables"),
1473
+ steps: [
1474
+ {
1475
+ id: "create",
1476
+ label: tr("fluidgrids.dataTables.nextStepCreateLabel", "Create a table"),
1477
+ description: tr("fluidgrids.dataTables.nextStepCreateDescription", "Define typed columns for the records your workflow needs."),
1478
+ icon: /* @__PURE__ */ jsx(Plus, {
1479
+ className: "size-4",
1480
+ "aria-hidden": "true"
1481
+ }),
1482
+ onClick: canCreateTable ? () => {
1483
+ setEditingTable(null);
1484
+ setTableDialogOpen(true);
1485
+ } : void 0
1486
+ },
1487
+ {
1488
+ id: "connect",
1489
+ label: tr("fluidgrids.dataTables.nextStepConnectLabel", "Connect it to a workflow"),
1490
+ description: tr("fluidgrids.dataTables.nextStepConnectDescription", "Add a Data Table node to get, insert, update, upsert, or delete rows."),
1491
+ icon: /* @__PURE__ */ jsx(Table2, {
1492
+ className: "size-4",
1493
+ "aria-hidden": "true"
1494
+ }),
1495
+ onClick: () => navigate("../list")
1496
+ },
1497
+ {
1498
+ id: "reuse",
1499
+ label: tr("fluidgrids.dataTables.nextStepReuseLabel", "Reuse data across runs"),
1500
+ description: tr("fluidgrids.dataTables.nextStepReuseDescription", "Use saved status, cursors, lookups, and deduplication keys on later runs."),
1501
+ icon: /* @__PURE__ */ jsx(RefreshCw, {
1502
+ className: "size-4",
1503
+ "aria-hidden": "true"
1504
+ })
1505
+ }
1506
+ ]
1507
+ }),
1508
+ /* @__PURE__ */ jsxs("div", {
1509
+ className: "grid gap-5 xl:grid-cols-[minmax(16rem,22rem)_minmax(0,1fr)]",
1510
+ children: [
1511
+ /* @__PURE__ */ jsxs(Card, {
1512
+ className: "h-fit border-border-secondary",
1513
+ "data-tour": "fluidgrids-data-tables-list",
1514
+ children: [/* @__PURE__ */ jsxs(CardHeader, {
1515
+ className: "gap-3 border-b border-border-secondary p-4",
1516
+ children: [
1517
+ /* @__PURE__ */ jsxs("div", {
1518
+ className: "flex items-center justify-between gap-2",
1519
+ children: [/* @__PURE__ */ jsx(CardTitle, {
1520
+ className: "text-base",
1521
+ children: tr("fluidgrids.dataTables.tables", "Tables")
1522
+ }), /* @__PURE__ */ jsx(Badge, {
1523
+ variant: "outline",
1524
+ children: total
1525
+ })]
1526
+ }),
1527
+ /* @__PURE__ */ jsxs("form", {
1528
+ className: "flex gap-2",
1529
+ onSubmit: submitSearch,
1530
+ children: [/* @__PURE__ */ jsxs("div", {
1531
+ className: "relative min-w-0 flex-1",
1532
+ children: [/* @__PURE__ */ jsx(Search, {
1533
+ className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-text-secondary",
1534
+ "aria-hidden": "true"
1535
+ }), /* @__PURE__ */ jsx(Input, {
1536
+ className: "pl-8",
1537
+ value: searchInput,
1538
+ onChange: (event) => setSearchInput(event.target.value),
1539
+ placeholder: tr("fluidgrids.dataTables.search", "Search tables"),
1540
+ "aria-label": tr("fluidgrids.dataTables.search", "Search tables")
1541
+ })]
1542
+ }), /* @__PURE__ */ jsx(Button$1, {
1543
+ type: "submit",
1544
+ variant: "outline",
1545
+ size: "sm",
1546
+ children: tr("fluidgrids.dataTables.searchAction", "Search")
1547
+ })]
1548
+ }),
1549
+ /* @__PURE__ */ jsx(TagFilterSelect, {
1550
+ value: tagFilter,
1551
+ onChange: setTagFilter,
1552
+ className: "w-full"
1553
+ })
1554
+ ]
1555
+ }), /* @__PURE__ */ jsxs(CardContent, {
1556
+ className: "max-h-72 overflow-y-auto p-2 xl:max-h-[36rem]",
1557
+ children: [
1558
+ loading && /* @__PURE__ */ jsx("p", {
1559
+ className: "p-4 text-sm text-text-secondary",
1560
+ children: tr("fluidgrids.dataTables.loading", "Loading tables…")
1561
+ }),
1562
+ !loading && tables.length === 0 && tagFilter && /* @__PURE__ */ jsx("p", {
1563
+ className: "p-4 text-sm text-text-secondary",
1564
+ children: tr("fluidgrids.dataTables.noTablesWithTag", "No tables carry this tag.")
1565
+ }),
1566
+ !loading && tables.length === 0 && !tagFilter && /* @__PURE__ */ jsx(IllustratedEmptyState, {
1567
+ className: "py-8",
1568
+ illustration: "onboarding",
1569
+ title: tr("fluidgrids.dataTables.empty", "Create your first Data Table"),
1570
+ description: tr("fluidgrids.dataTables.emptyHint", "Give your workflows a shared place to store typed records across runs."),
1571
+ action: /* @__PURE__ */ jsx(SdkPermissionButton, {
1572
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE,
1573
+ deniedTooltip: tr("fluidgrids.dataTables.createDeniedTooltip", "You don't have permission to create data tables"),
1574
+ children: /* @__PURE__ */ jsxs(Button$1, {
1575
+ size: "sm",
1576
+ onClick: () => {
1577
+ setEditingTable(null);
1578
+ setTableDialogOpen(true);
1579
+ },
1580
+ children: [/* @__PURE__ */ jsx(Plus, {
1581
+ className: "mr-2 size-4",
1582
+ "aria-hidden": "true"
1583
+ }), tr("fluidgrids.dataTables.createFirstTable", "Create a table")]
1584
+ })
1585
+ })
1586
+ }),
1587
+ !loading && tables.map((table) => /* @__PURE__ */ jsxs("button", {
1588
+ type: "button",
1589
+ onClick: () => setSelectedId(table.id),
1590
+ className: `mb-1 flex w-full items-start justify-between gap-3 rounded-md px-3 py-3 text-left transition-colors ${selectedId === table.id ? "bg-background-tertiary text-text-primary" : "text-text-secondary hover:bg-background-secondary hover:text-text-primary"}`,
1591
+ children: [/* @__PURE__ */ jsxs("span", {
1592
+ className: "min-w-0",
1593
+ children: [
1594
+ /* @__PURE__ */ jsx("span", {
1595
+ className: "block truncate text-sm font-medium",
1596
+ children: table.name
1597
+ }),
1598
+ /* @__PURE__ */ jsxs("span", {
1599
+ className: "mt-1 block text-xs text-text-secondary",
1600
+ children: [
1601
+ table.rowCount,
1602
+ " ",
1603
+ tr("fluidgrids.dataTables.rows", "rows"),
1604
+ " ·",
1605
+ " ",
1606
+ table.columns.length,
1607
+ " ",
1608
+ tr("fluidgrids.dataTables.columns", "columns")
1609
+ ]
1610
+ }),
1611
+ (tagsById[table.id] ?? []).length > 0 && /* @__PURE__ */ jsx("span", {
1612
+ className: "mt-1.5 flex flex-wrap gap-1",
1613
+ children: (tagsById[table.id] ?? []).map((assignment) => /* @__PURE__ */ jsx(TagBadge, {
1614
+ id: assignment.tagId,
1615
+ label: tagLabel(assignment),
1616
+ color: assignment.tagInfo?.color ?? void 0,
1617
+ size: "sm"
1618
+ }, assignment.tagId))
1619
+ })
1620
+ ]
1621
+ }), /* @__PURE__ */ jsx(Table2, {
1622
+ className: "mt-0.5 size-4 shrink-0",
1623
+ "aria-hidden": "true"
1624
+ })]
1625
+ }, table.id))
1626
+ ]
1627
+ })]
1628
+ }),
1629
+ error && !selectedTable && /* @__PURE__ */ jsx(Card, {
1630
+ className: "border-border-secondary",
1631
+ children: /* @__PURE__ */ jsxs(CardContent, {
1632
+ className: "p-6 text-sm text-text-secondary",
1633
+ children: [/* @__PURE__ */ jsx("p", { children: error.message }), /* @__PURE__ */ jsxs(Button$1, {
1634
+ className: "mt-4",
1635
+ variant: "outline",
1636
+ onClick: () => void refresh(),
1637
+ children: [/* @__PURE__ */ jsx(RefreshCw, {
1638
+ className: "mr-2 size-4",
1639
+ "aria-hidden": "true"
1640
+ }), tr("fluidgrids.dataTables.retry", "Try again")]
1641
+ })]
1642
+ })
1643
+ }),
1644
+ !error && !selectedTable && !loading && /* @__PURE__ */ jsx(Card, {
1645
+ className: "border-border-secondary",
1646
+ children: /* @__PURE__ */ jsxs(CardContent, {
1647
+ className: "flex min-h-[22rem] flex-col items-center justify-center p-6 text-center",
1648
+ children: [
1649
+ /* @__PURE__ */ jsx(Database, {
1650
+ className: "mb-3 size-10 text-text-secondary",
1651
+ "aria-hidden": "true"
1652
+ }),
1653
+ /* @__PURE__ */ jsx("h2", {
1654
+ className: "text-lg font-semibold text-text-primary",
1655
+ children: tr("fluidgrids.dataTables.selectTitle", "Select a data table")
1656
+ }),
1657
+ /* @__PURE__ */ jsx("p", {
1658
+ className: "mt-1 max-w-sm text-sm text-text-secondary",
1659
+ children: tr("fluidgrids.dataTables.selectDescription", "Choose a table to view and edit its rows, or create your first table.")
1660
+ })
1661
+ ]
1662
+ })
1663
+ }),
1664
+ selectedTable && /* @__PURE__ */ jsx("div", {
1665
+ className: "min-w-0",
1666
+ children: /* @__PURE__ */ jsx(TableRowsPanel, {
1667
+ table: selectedTable,
1668
+ onTableChanged: refresh,
1669
+ onEditTable: () => {
1670
+ setEditingTable(selectedTable);
1671
+ setTableDialogOpen(true);
1672
+ },
1673
+ onDeleteTable: handleDeleteTable,
1674
+ deletingTable: deleting,
1675
+ onTagsChanged: handleTagsChanged
1676
+ }, selectedTable.id)
1677
+ })
1678
+ ]
1679
+ })
1680
+ ]
1681
+ }), /* @__PURE__ */ jsx(TableFormDialog, {
1682
+ open: tableDialogOpen,
1683
+ table: editingTable,
1684
+ onOpenChange: setTableDialogOpen,
1685
+ onSaved: handleTableSaved
1686
+ })]
1687
+ });
1688
+ }
1689
+
1690
+ export { rowsToCsv as a, useDataTables as c, useInsertDataTableRow as d, useUpdateDataTable as f, parseCsv as i, useDeleteDataTable as l, DataTablesPage_exports as n, useCreateDataTable as o, useUpdateDataTableRow as p, csvToRows as r, useDataTableRows as s, DataTablesPage as t, useDeleteDataTableRows as u };