@burdenoff/fluidgrids-fe-sdk 2026.1112.6 → 2026.1112.7

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 (32) hide show
  1. package/dist/{AiStudioPage-BSLdKT0H.js → AiStudioPage-QGTFaVpx.js} +1 -1
  2. package/dist/ConfirmActionDialog-D9Pir3bc.js +30 -0
  3. package/dist/DataTablesPage-0U89KNBt.js +2880 -0
  4. package/dist/{ExecutionDetailPage-B9eEep77.js → ExecutionDetailPage-D4Mdbzb2.js} +2 -1
  5. package/dist/{ExecutionsListPage-DuuPSVEj.js → ExecutionsListPage-g_SPxAGX.js} +2 -1
  6. package/dist/RunActionsMenu-DmO14hEE.js +106 -0
  7. package/dist/RunnersPage-CvAPHdfU.js +673 -0
  8. package/dist/{TemplatePreview-BFMsDQif.js → TemplatePreview-DY4GxnfC.js} +1 -1
  9. package/dist/{WorkflowBuilder-D9W8hQTO.js → WorkflowBuilder-Aslfa2vj.js} +1 -1
  10. package/dist/{WorkflowBuilder-B8PtcT0Q.js → WorkflowBuilder-DY1_KJiV.js} +1 -1
  11. package/dist/{agentTools-DlPHMfhA.js → agentTools-B8gnHFVM.js} +1 -1
  12. package/dist/{canvasNoteTools-CkAsMSvt.js → canvasNoteTools-Cj7MvVDH.js} +1 -1
  13. package/dist/executions.js +2 -2
  14. package/dist/{flow-builder-DUkCKl-S.js → flow-builder-dvqpKs8d.js} +5 -5
  15. package/dist/{index-u-kM0seP.d.ts → index-DNo9aFUT.d.ts} +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +13 -13
  18. package/dist/{node-detail-view-ApMQwQRQ.js → node-detail-view-ZnJ3Hvvo.js} +2 -2
  19. package/dist/{node-editor-tab-body-DIcmar84.js → node-editor-tab-body-BuiIr7VD.js} +1 -1
  20. package/dist/observability.d.ts +1 -1
  21. package/dist/{popover-DOeSGQOG.js → popover-mexhIFP3.js} +3 -0
  22. package/dist/{quick-config-panel-Pm5nkZVr.js → quick-config-panel-EV66lJOM.js} +2 -2
  23. package/dist/{schedule-panel-Dizqz9eA.js → schedule-panel-BeLfDNFh.js} +1 -1
  24. package/dist/{sidebar-node-editor-BNub2vUq.js → sidebar-node-editor-D85Moj5d.js} +2 -2
  25. package/dist/templates.js +1 -1
  26. package/dist/{useDurableParity-Cb9nOdE8.js → useDurableParity-04-5PTk2.js} +3 -101
  27. package/dist/workflow-builder.js +2 -2
  28. package/dist/workflow-pages.d.ts +1 -1
  29. package/dist/workflow-pages.js +7 -7
  30. package/package.json +1 -1
  31. package/dist/DataTablesPage-Dm-dzVPD.js +0 -1698
  32. package/dist/RunnersPage-D9Bgod_d.js +0 -467
@@ -0,0 +1,2880 @@
1
+ "use client";
2
+ import { t as __exportAll } from "./rolldown-runtime-hDbaFkMy.js";
3
+ import { t as EntityTagPicker } from "./EntityTagPicker-dxfzv8aO.js";
4
+ import { t as TagFilterSelect } from "./TagFilterSelect-CqhxcDjh.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-D5C0tdLs.js";
7
+ import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
8
+ import { i as useOpenRequest, n as ACTION_PARAM_VALUES, r as useActionParam } from "./useActionParam-B0ebTZmg.js";
9
+ import { i as SdkPermissionButton, s as useFluidGridsPermission, t as FLUIDGRIDS_PERMISSIONS } from "./constants-dcxg6zG6.js";
10
+ import { t as FluidGridsDocsLink } from "./FluidGridsDocsLink-JxkBpOeb.js";
11
+ import { n as useNavigationV2 } from "./useNavigationV2-C2WeqN7d.js";
12
+ import { t as ConfirmActionDialog } from "./ConfirmActionDialog-D9Pir3bc.js";
13
+ import { n as TechnicalDetails, r as Disclosure, t as RunActionsMenu } from "./RunActionsMenu-DmO14hEE.js";
14
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
+ import { useNavigate } from "react-router";
16
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
17
+ import { toast } from "sonner";
18
+ import { gql } from "@apollo/client";
19
+ import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, IllustratedEmptyState, Input, Label, NextSteps, PagePurpose, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@burdenoff/fe-libs/ui";
20
+ import { Button as Button$1 } from "@burdenoff/fe-libs/button";
21
+ import { ArrowDownToLine, ArrowLeft, ArrowUpFromLine, ChevronLeft, ChevronRight, Database, Pencil, Plus, RefreshCw, Search, Table2, Trash2 } from "lucide-react";
22
+ import { TagBadge } from "@burdenoff/fe-libs/tag";
23
+
24
+ // src/modules/data-tables/hooks/useDataTables.ts
25
+ const DATA_TABLE_FIELDS = gql`
26
+ fragment DataTableFields on DataTable {
27
+ id
28
+ workspaceId
29
+ name
30
+ description
31
+ columns {
32
+ key
33
+ label
34
+ type
35
+ required
36
+ defaultValue
37
+ }
38
+ rowCount
39
+ sizeBytes
40
+ createdAt
41
+ updatedAt
42
+ createdBy
43
+ updatedBy
44
+ }
45
+ `;
46
+ const DATA_TABLE_ROW_FIELDS = gql`
47
+ fragment DataTableRowFields on DataTableRow {
48
+ id
49
+ tableId
50
+ data
51
+ createdAt
52
+ updatedAt
53
+ }
54
+ `;
55
+ const GET_DATA_TABLES = gql`
56
+ ${DATA_TABLE_FIELDS}
57
+ query GetDataTables($search: String, $limit: Int, $offset: Int) {
58
+ dataTables(search: $search, limit: $limit, offset: $offset) {
59
+ tables {
60
+ ...DataTableFields
61
+ }
62
+ total
63
+ }
64
+ }
65
+ `;
66
+ const GET_DATA_TABLES_BY_TAG = gql`
67
+ ${DATA_TABLE_FIELDS}
68
+ query GetDataTablesByTag(
69
+ $search: String
70
+ $limit: Int
71
+ $offset: Int
72
+ $tagIds: [ID!]
73
+ ) {
74
+ dataTables(
75
+ search: $search
76
+ limit: $limit
77
+ offset: $offset
78
+ tagIds: $tagIds
79
+ ) {
80
+ tables {
81
+ ...DataTableFields
82
+ }
83
+ total
84
+ }
85
+ }
86
+ `;
87
+ const GET_DATA_TABLE_ROWS = gql`
88
+ ${DATA_TABLE_ROW_FIELDS}
89
+ query GetDataTableRows($input: DataTableRowsQueryInput!) {
90
+ dataTableRows(input: $input) {
91
+ rows {
92
+ ...DataTableRowFields
93
+ }
94
+ total
95
+ }
96
+ }
97
+ `;
98
+ const CREATE_DATA_TABLE = gql`
99
+ ${DATA_TABLE_FIELDS}
100
+ mutation CreateDataTable($input: CreateDataTableInput!) {
101
+ createDataTable(input: $input) {
102
+ success
103
+ message
104
+ table {
105
+ ...DataTableFields
106
+ }
107
+ }
108
+ }
109
+ `;
110
+ const UPDATE_DATA_TABLE = gql`
111
+ ${DATA_TABLE_FIELDS}
112
+ mutation UpdateDataTable($id: ID!, $input: UpdateDataTableInput!) {
113
+ updateDataTable(id: $id, input: $input) {
114
+ success
115
+ message
116
+ table {
117
+ ...DataTableFields
118
+ }
119
+ }
120
+ }
121
+ `;
122
+ const DELETE_DATA_TABLE = gql`
123
+ mutation DeleteDataTable($id: ID!) {
124
+ deleteDataTable(id: $id) {
125
+ success
126
+ message
127
+ }
128
+ }
129
+ `;
130
+ const INSERT_DATA_TABLE_ROW = gql`
131
+ ${DATA_TABLE_ROW_FIELDS}
132
+ mutation InsertDataTableRow($tableId: ID!, $data: JSON!) {
133
+ insertDataTableRow(tableId: $tableId, data: $data) {
134
+ success
135
+ message
136
+ row {
137
+ ...DataTableRowFields
138
+ }
139
+ }
140
+ }
141
+ `;
142
+ const UPDATE_DATA_TABLE_ROW = gql`
143
+ ${DATA_TABLE_ROW_FIELDS}
144
+ mutation UpdateDataTableRow($id: ID!, $data: JSON!) {
145
+ updateDataTableRow(id: $id, data: $data) {
146
+ success
147
+ message
148
+ row {
149
+ ...DataTableRowFields
150
+ }
151
+ }
152
+ }
153
+ `;
154
+ const DELETE_DATA_TABLE_ROWS = gql`
155
+ mutation DeleteDataTableRows($tableId: ID!, $ids: [ID!]!) {
156
+ deleteDataTableRows(tableId: $tableId, ids: $ids) {
157
+ success
158
+ message
159
+ affected
160
+ }
161
+ }
162
+ `;
163
+ function toRow(row) {
164
+ return {
165
+ id: row.id,
166
+ tableId: row.tableId,
167
+ data: toRecord(row.data),
168
+ createdAt: row.createdAt,
169
+ updatedAt: row.updatedAt
170
+ };
171
+ }
172
+ function toRecord(value) {
173
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
174
+ }
175
+ function operationError(error, fallback) {
176
+ return new Error(describeOperationError(error, fallback));
177
+ }
178
+ /** A workspace holds at most 100 tables and the service serves at most 100 per request. */
179
+ const DATA_TABLES_LIST_LIMIT = 100;
180
+ /** The workspace's data tables, optionally narrowed by name and by one tag (G47). */
181
+ function useDataTables(search, tagId) {
182
+ const [tables, setTables] = useState([]);
183
+ const [total, setTotal] = useState(0);
184
+ const [loading, setLoading] = useState(true);
185
+ const [error, setError] = useState(null);
186
+ const fetchTables = useCallback(async () => {
187
+ setLoading(true);
188
+ setError(null);
189
+ try {
190
+ const result = throwIfResultError(await getFluidGridsClient().query({
191
+ query: tagId ? GET_DATA_TABLES_BY_TAG : GET_DATA_TABLES,
192
+ variables: {
193
+ search: search?.trim() || void 0,
194
+ limit: 100,
195
+ offset: 0,
196
+ ...tagId ? { tagIds: [tagId] } : {}
197
+ },
198
+ fetchPolicy: "network-only"
199
+ }));
200
+ setTables(result.data?.dataTables.tables ?? []);
201
+ setTotal(result.data?.dataTables.total ?? 0);
202
+ } catch (err) {
203
+ setError(operationError(err, "Failed to load data tables"));
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ }, [search, tagId]);
208
+ useEffect(() => {
209
+ fetchTables();
210
+ }, [fetchTables]);
211
+ return {
212
+ tables,
213
+ total,
214
+ loading,
215
+ error,
216
+ refetch: fetchTables
217
+ };
218
+ }
219
+ /** Largest page the service serves for a single `dataTableRows` request. */
220
+ const DATA_TABLE_MAX_PAGE_SIZE = 5e3;
221
+ async function queryRowsPage(tableId, limit, offset) {
222
+ const response = throwIfResultError(await getFluidGridsClient().query({
223
+ query: GET_DATA_TABLE_ROWS,
224
+ variables: { input: {
225
+ tableId,
226
+ limit,
227
+ offset
228
+ } },
229
+ fetchPolicy: "network-only"
230
+ })).data?.dataTableRows;
231
+ return {
232
+ rows: response?.rows.map(toRow) ?? [],
233
+ total: response?.total ?? 0
234
+ };
235
+ }
236
+ /**
237
+ * Fetch every row of a table in service-sized batches (used by CSV export so
238
+ * it covers the whole table rather than the page currently on screen). A table
239
+ * is capped at 50k rows server-side, so this is at most ten requests.
240
+ */
241
+ async function fetchAllDataTableRows(tableId) {
242
+ const collected = [];
243
+ let total = Number.POSITIVE_INFINITY;
244
+ while (collected.length < total) {
245
+ const page = await queryRowsPage(tableId, DATA_TABLE_MAX_PAGE_SIZE, collected.length);
246
+ total = page.total;
247
+ if (page.rows.length === 0) break;
248
+ collected.push(...page.rows);
249
+ }
250
+ return collected;
251
+ }
252
+ function useDataTableRows(tableId, paging) {
253
+ const { page, pageSize } = paging;
254
+ const [rows, setRows] = useState([]);
255
+ const [total, setTotal] = useState(0);
256
+ const [loading, setLoading] = useState(false);
257
+ const [error, setError] = useState(null);
258
+ const fetchRows = useCallback(async () => {
259
+ if (!tableId) {
260
+ setRows([]);
261
+ setTotal(0);
262
+ setLoading(false);
263
+ setError(null);
264
+ return;
265
+ }
266
+ setLoading(true);
267
+ setError(null);
268
+ try {
269
+ const response = await queryRowsPage(tableId, pageSize, page * pageSize);
270
+ setRows(response.rows);
271
+ setTotal(response.total);
272
+ } catch (err) {
273
+ setError(operationError(err, "Failed to load data table rows"));
274
+ } finally {
275
+ setLoading(false);
276
+ }
277
+ }, [
278
+ tableId,
279
+ page,
280
+ pageSize
281
+ ]);
282
+ useEffect(() => {
283
+ fetchRows();
284
+ }, [fetchRows]);
285
+ return {
286
+ rows,
287
+ total,
288
+ loading,
289
+ error,
290
+ refetch: fetchRows
291
+ };
292
+ }
293
+ function useCreateDataTable() {
294
+ const [loading, setLoading] = useState(false);
295
+ return {
296
+ createDataTable: useCallback(async (input) => {
297
+ setLoading(true);
298
+ try {
299
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
300
+ mutation: CREATE_DATA_TABLE,
301
+ variables: { input }
302
+ })).data?.createDataTable;
303
+ if (!response?.success) throw new Error(response?.message || "Failed to create data table");
304
+ return response.table ?? null;
305
+ } finally {
306
+ setLoading(false);
307
+ }
308
+ }, []),
309
+ loading
310
+ };
311
+ }
312
+ function useUpdateDataTable() {
313
+ const [loading, setLoading] = useState(false);
314
+ return {
315
+ updateDataTable: useCallback(async (id, input) => {
316
+ setLoading(true);
317
+ try {
318
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
319
+ mutation: UPDATE_DATA_TABLE,
320
+ variables: {
321
+ id,
322
+ input
323
+ }
324
+ })).data?.updateDataTable;
325
+ if (!response?.success) throw new Error(response?.message || "Failed to update data table");
326
+ return response.table ?? null;
327
+ } finally {
328
+ setLoading(false);
329
+ }
330
+ }, []),
331
+ loading
332
+ };
333
+ }
334
+ function useDeleteDataTable() {
335
+ const [loading, setLoading] = useState(false);
336
+ return {
337
+ deleteDataTable: useCallback(async (id) => {
338
+ setLoading(true);
339
+ try {
340
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
341
+ mutation: DELETE_DATA_TABLE,
342
+ variables: { id }
343
+ })).data?.deleteDataTable;
344
+ if (!response?.success) throw new Error(response?.message || "Failed to delete data table");
345
+ return true;
346
+ } finally {
347
+ setLoading(false);
348
+ }
349
+ }, []),
350
+ loading
351
+ };
352
+ }
353
+ function useInsertDataTableRow() {
354
+ const [loading, setLoading] = useState(false);
355
+ return {
356
+ insertDataTableRow: useCallback(async (tableId, data) => {
357
+ setLoading(true);
358
+ try {
359
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
360
+ mutation: INSERT_DATA_TABLE_ROW,
361
+ variables: {
362
+ tableId,
363
+ data
364
+ }
365
+ })).data?.insertDataTableRow;
366
+ if (!response?.success) throw new Error(response?.message || "Failed to insert row");
367
+ return response.row ? toRow(response.row) : null;
368
+ } finally {
369
+ setLoading(false);
370
+ }
371
+ }, []),
372
+ loading
373
+ };
374
+ }
375
+ function useUpdateDataTableRow() {
376
+ const [loading, setLoading] = useState(false);
377
+ return {
378
+ updateDataTableRow: useCallback(async (id, data) => {
379
+ setLoading(true);
380
+ try {
381
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
382
+ mutation: UPDATE_DATA_TABLE_ROW,
383
+ variables: {
384
+ id,
385
+ data
386
+ }
387
+ })).data?.updateDataTableRow;
388
+ if (!response?.success) throw new Error(response?.message || "Failed to update row");
389
+ return response.row ? toRow(response.row) : null;
390
+ } finally {
391
+ setLoading(false);
392
+ }
393
+ }, []),
394
+ loading
395
+ };
396
+ }
397
+ function useDeleteDataTableRows() {
398
+ const [loading, setLoading] = useState(false);
399
+ return {
400
+ deleteDataTableRows: useCallback(async (tableId, ids) => {
401
+ setLoading(true);
402
+ try {
403
+ const response = throwIfResultError(await getFluidGridsClient().mutate({
404
+ mutation: DELETE_DATA_TABLE_ROWS,
405
+ variables: {
406
+ tableId,
407
+ ids
408
+ }
409
+ })).data?.deleteDataTableRows;
410
+ if (!response?.success) throw new Error(response?.message || "Failed to delete rows");
411
+ return response.affected;
412
+ } finally {
413
+ setLoading(false);
414
+ }
415
+ }, []),
416
+ loading
417
+ };
418
+ }
419
+
420
+ // src/modules/data-tables/utils/values.ts
421
+ const TRUE_WORDS = /* @__PURE__ */ new Set([
422
+ "true",
423
+ "yes",
424
+ "1"
425
+ ]);
426
+ const FALSE_WORDS = /* @__PURE__ */ new Set([
427
+ "false",
428
+ "no",
429
+ "0"
430
+ ]);
431
+ /** "true"/"yes"/"1" and "false"/"no"/"0", any case; `undefined` for anything else. */
432
+ function parseBooleanText(value) {
433
+ const normalized = value.trim().toLowerCase();
434
+ if (TRUE_WORDS.has(normalized)) return true;
435
+ if (FALSE_WORDS.has(normalized)) return false;
436
+ }
437
+ const DATE_ONLY$1 = /^\d{4}-\d{2}-\d{2}$/;
438
+ const HAS_ZONE = /(?:Z|[+-]\d{2}:?\d{2})$/i;
439
+ /**
440
+ * A date as the service stores it. A date without a time stays as typed; a
441
+ * date with a time but no zone is read in the viewer's zone and saved with the
442
+ * zone (UTC) so it means the same moment for everyone. Throws on a value that
443
+ * is not a date.
444
+ */
445
+ function normalizeDateValue(value) {
446
+ const trimmed = value.trim();
447
+ if (DATE_ONLY$1.test(trimmed)) {
448
+ if (Number.isNaN(Date.parse(`${trimmed}T00:00:00Z`))) throw new Error(`Expected a date, got "${value}"`);
449
+ return trimmed;
450
+ }
451
+ const parsed = new Date(trimmed);
452
+ if (Number.isNaN(parsed.getTime())) throw new Error(`Expected a date, got "${value}"`);
453
+ return HAS_ZONE.test(trimmed) ? trimmed : parsed.toISOString();
454
+ }
455
+ function pad(number) {
456
+ return String(number).padStart(2, "0");
457
+ }
458
+ /** A stored date as a `datetime-local` input value (viewer's zone); "" if unreadable. */
459
+ function dateToLocalInput(value) {
460
+ if (typeof value !== "string" || value === "") return "";
461
+ const text = DATE_ONLY$1.test(value) ? `${value}T00:00` : value;
462
+ const parsed = new Date(text);
463
+ if (Number.isNaN(parsed.getTime())) return "";
464
+ return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}T${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`;
465
+ }
466
+ /** The text a row-form input starts with for a stored value. */
467
+ function asFormValue(value, type) {
468
+ if (value === null || value === void 0) return "";
469
+ if (type === "JSON") return JSON.stringify(value, null, 2);
470
+ if (type === "DATE") return dateToLocalInput(value);
471
+ return String(value);
472
+ }
473
+ /** The value a row-form input saves; throws a plain message when it is not valid. */
474
+ function parseFormValue(value, type) {
475
+ if (type === "STRING") return value;
476
+ if (value.trim() === "") return null;
477
+ if (type === "DATE") return normalizeDateValue(value);
478
+ if (type === "NUMBER") {
479
+ const number = Number(value);
480
+ if (!Number.isFinite(number)) throw new Error(`"${value}" is not a number`);
481
+ return number;
482
+ }
483
+ if (type === "BOOLEAN") {
484
+ const parsed = parseBooleanText(value);
485
+ if (parsed === void 0) throw new Error("Boolean values must be true or false");
486
+ return parsed;
487
+ }
488
+ try {
489
+ return JSON.parse(value);
490
+ } catch {
491
+ throw new Error("JSON values must contain valid JSON");
492
+ }
493
+ }
494
+
495
+ // src/modules/data-tables/utils/csv.ts
496
+ /** Parse RFC 4180-style CSV while preserving quoted commas and line breaks. */
497
+ function parseCsv(text) {
498
+ const rows = [];
499
+ let row = [];
500
+ let cell = "";
501
+ let quoted = false;
502
+ for (let index = 0; index < text.length; index += 1) {
503
+ const character = text[index];
504
+ const next = text[index + 1];
505
+ if (quoted) {
506
+ if (character === "\"" && next === "\"") {
507
+ cell += "\"";
508
+ index += 1;
509
+ } else if (character === "\"") quoted = false;
510
+ else cell += character;
511
+ continue;
512
+ }
513
+ if (character === "\"" && cell.length === 0) quoted = true;
514
+ else if (character === ",") {
515
+ row.push(cell);
516
+ cell = "";
517
+ } else if (character === "\n" || character === "\r") {
518
+ if (character === "\r" && next === "\n") index += 1;
519
+ row.push(cell);
520
+ cell = "";
521
+ if (row.some((value) => value.length > 0)) rows.push(row);
522
+ row = [];
523
+ } else cell += character;
524
+ }
525
+ if (quoted) throw new Error("CSV contains an unterminated quoted value");
526
+ if (cell.length > 0 || row.length > 0) {
527
+ row.push(cell);
528
+ if (row.some((value) => value.length > 0)) rows.push(row);
529
+ }
530
+ return rows;
531
+ }
532
+ function parseValue(value, type) {
533
+ if (value === "") return type === "STRING" ? "" : null;
534
+ switch (type) {
535
+ case "NUMBER": {
536
+ const parsed = Number(value);
537
+ if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got "${value}"`);
538
+ return parsed;
539
+ }
540
+ case "BOOLEAN": {
541
+ const parsed = parseBooleanText(value);
542
+ if (parsed === void 0) throw new Error(`Expected true or false, got "${value}"`);
543
+ return parsed;
544
+ }
545
+ case "JSON": try {
546
+ return JSON.parse(value);
547
+ } catch {
548
+ throw new Error(`Expected valid JSON, got "${value}"`);
549
+ }
550
+ case "DATE": return normalizeDateValue(value);
551
+ case "STRING": return value;
552
+ }
553
+ }
554
+ /** Convert a CSV whose first row contains column keys into validated row data. */
555
+ function csvToRows(text, columns) {
556
+ const parsed = parseCsv(text);
557
+ if (parsed.length < 2) return [];
558
+ const headers = parsed[0].map((header) => header.trim());
559
+ const columnByKey = new Map(columns.map((column) => [column.key, column]));
560
+ const columnByLabel = new Map(columns.map((column) => [column.label, column]));
561
+ const seen = /* @__PURE__ */ new Set();
562
+ const headerColumns = headers.map((header) => {
563
+ const column = columnByKey.get(header) ?? columnByLabel.get(header);
564
+ if (!header || column && seen.has(column.key)) throw new Error("CSV must have unique, non-empty column headers");
565
+ if (!column) throw new Error(`CSV contains an unknown column "${header}"`);
566
+ seen.add(column.key);
567
+ return column;
568
+ });
569
+ return parsed.slice(1).map((values, rowIndex) => {
570
+ const row = {};
571
+ headerColumns.forEach((column, columnIndex) => {
572
+ const rawValue = values[columnIndex] ?? "";
573
+ try {
574
+ row[column.key] = parseValue(rawValue, column.type);
575
+ } catch (error) {
576
+ const message = error instanceof Error ? error.message : "Invalid value";
577
+ throw new Error(`Row ${rowIndex + 2}, ${column.label}: ${message}`);
578
+ }
579
+ });
580
+ return row;
581
+ });
582
+ }
583
+ function csvCell(value) {
584
+ if (value === null || value === void 0) return "";
585
+ const text = typeof value === "object" ? JSON.stringify(value) : String(value);
586
+ return /[",\r\n]/.test(text) ? `"${text.replaceAll("\"", "\"\"")}"` : text;
587
+ }
588
+ /** Serialize table rows with a stable header matching the table schema. */
589
+ function rowsToCsv(columns, rows) {
590
+ return [columns.map((column) => csvCell(column.key)).join(","), ...rows.map((row) => columns.map((column) => csvCell(row.data[column.key])).join(","))].join("\r\n");
591
+ }
592
+
593
+ // src/modules/data-tables/utils/columns.ts
594
+ /** "Contact e-mail" -> "contact_e_mail"; never empty, never starts with a digit. */
595
+ function deriveColumnKey(name) {
596
+ const base = name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
597
+ if (base === "") return "column";
598
+ return /^[0-9]/.test(base) ? `column_${base}` : base;
599
+ }
600
+ /** `key`, or `key_2`, `key_3`… when `taken` already holds it. */
601
+ function uniqueColumnKey(key, taken) {
602
+ if (!taken.has(key)) return key;
603
+ let suffix = 2;
604
+ while (taken.has(`${key}_${suffix}`)) suffix += 1;
605
+ return `${key}_${suffix}`;
606
+ }
607
+ /**
608
+ * What a save would change that a workflow can notice: the table's name, and
609
+ * columns removed, renamed (key) or given another kind of data. Columns are
610
+ * matched by their position in `originalKeys` (the key each existing column
611
+ * had when the editor opened), so a key edit is not read as remove + add.
612
+ */
613
+ function describeSchemaChanges(table, next) {
614
+ const changes = [];
615
+ if (next.name.trim() !== table.name) changes.push({
616
+ kind: "renameTable",
617
+ from: table.name,
618
+ to: next.name.trim()
619
+ });
620
+ const kept = /* @__PURE__ */ new Set();
621
+ const beforeByKey = new Map(table.columns.map((c) => [c.key, c]));
622
+ for (const column of next.columns) {
623
+ if (column.originalKey === void 0) continue;
624
+ kept.add(column.originalKey);
625
+ const before = beforeByKey.get(column.originalKey);
626
+ if (!before) continue;
627
+ if (before.key !== column.key.trim()) changes.push({
628
+ kind: "changeKey",
629
+ from: before.key,
630
+ to: column.key.trim(),
631
+ label: before.label
632
+ });
633
+ if (before.type !== column.type) changes.push({
634
+ kind: "changeType",
635
+ key: before.key,
636
+ label: before.label,
637
+ from: before.type,
638
+ to: column.type
639
+ });
640
+ }
641
+ for (const before of table.columns) if (!kept.has(before.key)) changes.push({
642
+ kind: "removeColumn",
643
+ key: before.key,
644
+ label: before.label
645
+ });
646
+ return changes;
647
+ }
648
+ let columnUid = 0;
649
+ function nextColumnUid() {
650
+ columnUid += 1;
651
+ return `column-${columnUid}`;
652
+ }
653
+ /** The editor's columns for a table (or one blank column for a new table). */
654
+ function toEditableColumns(table, blank) {
655
+ if (!table) return [{
656
+ ...blank,
657
+ uid: nextColumnUid()
658
+ }];
659
+ return table.columns.map((column) => ({
660
+ key: column.key,
661
+ label: column.label,
662
+ type: column.type,
663
+ required: column.required,
664
+ ...column.defaultValue !== void 0 ? { defaultValue: column.defaultValue } : {},
665
+ uid: nextColumnUid(),
666
+ originalKey: column.key,
667
+ keyEdited: true
668
+ }));
669
+ }
670
+ /** Strip the editor-only fields and trim names before saving. */
671
+ function toColumnInputs(columns) {
672
+ return columns.map((column) => ({
673
+ key: column.key.trim(),
674
+ label: column.label.trim(),
675
+ type: column.type,
676
+ ...column.required !== void 0 ? { required: column.required } : {},
677
+ ...column.defaultValue !== void 0 ? { defaultValue: column.defaultValue } : {}
678
+ }));
679
+ }
680
+
681
+ // src/modules/data-tables/utils/format.ts
682
+ function formatBytes(bytes) {
683
+ if (bytes < 1024) return `${bytes} B`;
684
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
685
+ return `${(bytes / 1048576).toFixed(1)} MB`;
686
+ }
687
+ function formatValue(value) {
688
+ if (value === null || value === void 0) return "—";
689
+ if (typeof value === "object") return JSON.stringify(value);
690
+ return String(value);
691
+ }
692
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
693
+ const DATE_TIME_FORMAT = new Intl.DateTimeFormat(void 0, {
694
+ dateStyle: "medium",
695
+ timeStyle: "short"
696
+ });
697
+ const DATE_FORMAT = new Intl.DateTimeFormat(void 0, {
698
+ dateStyle: "medium",
699
+ timeZone: "UTC"
700
+ });
701
+ /** A cell as people read it: dates in their own zone, Yes / No as words. */
702
+ function formatCell(value, type, words) {
703
+ if (type === "BOOLEAN" && typeof value === "boolean") return value ? words.yes : words.no;
704
+ if (type === "DATE" && typeof value === "string") {
705
+ const parsed = new Date(value);
706
+ if (Number.isNaN(parsed.getTime())) return value;
707
+ return DATE_ONLY.test(value) ? DATE_FORMAT.format(parsed) : DATE_TIME_FORMAT.format(parsed);
708
+ }
709
+ return formatValue(value);
710
+ }
711
+ /**
712
+ * Order two cells of one column (`direction` 1 = A to Z, -1 = Z to A). Empty
713
+ * cells sort last either way.
714
+ */
715
+ function compareCells(a, b, type, direction = 1) {
716
+ const aEmpty = a === null || a === void 0 || a === "";
717
+ const bEmpty = b === null || b === void 0 || b === "";
718
+ if (aEmpty || bEmpty) return Number(aEmpty) - Number(bEmpty);
719
+ return direction * compareFilled(a, b, type);
720
+ }
721
+ function compareFilled(a, b, type) {
722
+ if (type === "NUMBER" && typeof a === "number" && typeof b === "number") return a - b;
723
+ if (type === "BOOLEAN") return Number(a === true) - Number(b === true);
724
+ if (type === "DATE") {
725
+ const diff = Date.parse(String(a)) - Date.parse(String(b));
726
+ if (!Number.isNaN(diff)) return diff;
727
+ }
728
+ return formatValue(a).localeCompare(formatValue(b), void 0, {
729
+ numeric: true,
730
+ sensitivity: "base"
731
+ });
732
+ }
733
+
734
+ // src/modules/data-tables/components/RowFormDialogV2.tsx
735
+ /**
736
+ * Add / Edit row dialog for Navigation v2 (BOFF-11849).
737
+ *
738
+ * The label is just the column name; a Yes / No column is a switch, a Date
739
+ * column a date-and-time picker, and a JSON column sits under "More options"
740
+ * (open from the start when it is required or already has a value). A new row
741
+ * starts from each column's starting value. Every value lives in this dialog's
742
+ * state, so closing "More options" never loses what was typed.
743
+ */
744
+ /** The values a row form starts with: the row's, or each column's starting value. */
745
+ function initialValues(table, row) {
746
+ const next = {};
747
+ for (const column of table?.columns ?? []) next[column.key] = asFormValue(row ? row.data[column.key] : column.defaultValue, column.type);
748
+ return next;
749
+ }
750
+ /**
751
+ * The shell only mounts the form while the dialog is open, and the form is
752
+ * keyed by the table and row, so reopening (or editing another row) always
753
+ * starts from fresh values.
754
+ */
755
+ function RowFormDialogV2({ open, table, row, onOpenChange, onSaved }) {
756
+ return /* @__PURE__ */ jsx(Dialog, {
757
+ open,
758
+ onOpenChange,
759
+ children: /* @__PURE__ */ jsx(DialogContent, {
760
+ className: "max-h-[90vh] max-w-2xl overflow-y-auto",
761
+ "data-testid": "row-form-dialog",
762
+ children: /* @__PURE__ */ jsx(RowFormBody, {
763
+ table,
764
+ row,
765
+ onOpenChange,
766
+ onSaved
767
+ }, `${table?.id ?? "none"}:${row?.id ?? "new"}`)
768
+ })
769
+ });
770
+ }
771
+ function RowFormBody({ table, row, onOpenChange, onSaved }) {
772
+ const tr = useTr();
773
+ const { insertDataTableRow, loading: inserting } = useInsertDataTableRow();
774
+ const { updateDataTableRow, loading: updating } = useUpdateDataTableRow();
775
+ const [values, setValues] = useState(() => initialValues(table, row));
776
+ const editing = row !== null;
777
+ const saving = inserting || updating;
778
+ const setValue = useCallback((key, value) => {
779
+ setValues((current) => ({
780
+ ...current,
781
+ [key]: value
782
+ }));
783
+ }, []);
784
+ const handleSubmit = useCallback(async () => {
785
+ if (!table) return;
786
+ try {
787
+ const data = {};
788
+ for (const column of table.columns) {
789
+ const value = values[column.key] ?? "";
790
+ if (column.required && value.trim() === "") throw new Error(`${column.label} is required`);
791
+ data[column.key] = parseFormValue(value, column.type);
792
+ }
793
+ if (editing && row) await updateDataTableRow(row.id, data);
794
+ else await insertDataTableRow(table.id, data);
795
+ toast.success(editing ? tr("fluidgrids.dataTables.rowUpdated", "Row updated") : tr("fluidgrids.dataTables.rowCreated", "Row added"));
796
+ onSaved();
797
+ onOpenChange(false);
798
+ } catch (error) {
799
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.rowSaveFailed", "Failed to save row")));
800
+ }
801
+ }, [
802
+ editing,
803
+ insertDataTableRow,
804
+ onOpenChange,
805
+ onSaved,
806
+ row,
807
+ table,
808
+ tr,
809
+ updateDataTableRow,
810
+ values
811
+ ]);
812
+ const everydayColumns = table?.columns.filter((c) => c.type !== "JSON") ?? [];
813
+ const jsonColumns = table?.columns.filter((c) => c.type === "JSON") ?? [];
814
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
815
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editRow", "Edit row") : tr("fluidgrids.dataTables.addRow", "Add row") }) }),
816
+ table && /* @__PURE__ */ jsxs("div", {
817
+ className: "space-y-4 py-2",
818
+ children: [everydayColumns.map((column) => {
819
+ const id = `data-table-row-${column.key}`;
820
+ const label = /* @__PURE__ */ jsxs(Label, {
821
+ htmlFor: id,
822
+ children: [column.label, column.required && /* @__PURE__ */ jsx("span", {
823
+ className: "ml-1 text-text-secondary",
824
+ children: "*"
825
+ })]
826
+ });
827
+ if (column.type === "BOOLEAN") return /* @__PURE__ */ jsxs("div", {
828
+ className: "flex items-center justify-between gap-3",
829
+ children: [label, /* @__PURE__ */ jsx(Switch, {
830
+ id,
831
+ checked: values[column.key] === "true",
832
+ onCheckedChange: (checked) => setValue(column.key, checked ? "true" : "false")
833
+ })]
834
+ }, column.key);
835
+ return /* @__PURE__ */ jsxs("div", {
836
+ className: "space-y-1.5",
837
+ children: [label, /* @__PURE__ */ jsx(Input, {
838
+ id,
839
+ type: column.type === "NUMBER" ? "number" : column.type === "DATE" ? "datetime-local" : "text",
840
+ value: values[column.key] ?? "",
841
+ onChange: (event) => setValue(column.key, event.target.value)
842
+ })]
843
+ }, column.key);
844
+ }), jsonColumns.length > 0 && /* @__PURE__ */ jsx(Disclosure, {
845
+ closedLabel: tr("fluidgrids.dataTables.moreOptions", "More options"),
846
+ openLabel: tr("fluidgrids.dataTables.fewerOptions", "Fewer options"),
847
+ defaultOpen: jsonColumns.some((c) => c.required || (row ? row.data[c.key] : c.defaultValue) != null),
848
+ children: /* @__PURE__ */ jsx("div", {
849
+ className: "space-y-4",
850
+ children: jsonColumns.map((column) => /* @__PURE__ */ jsxs("div", {
851
+ className: "space-y-1.5",
852
+ children: [/* @__PURE__ */ jsxs(Label, {
853
+ htmlFor: `data-table-row-${column.key}`,
854
+ children: [column.label, column.required && /* @__PURE__ */ jsx("span", {
855
+ className: "ml-1 text-text-secondary",
856
+ children: "*"
857
+ })]
858
+ }), /* @__PURE__ */ jsx(Textarea, {
859
+ id: `data-table-row-${column.key}`,
860
+ value: values[column.key] ?? "",
861
+ onChange: (event) => setValue(column.key, event.target.value),
862
+ rows: 4,
863
+ placeholder: "{ }"
864
+ })]
865
+ }, column.key))
866
+ })
867
+ })]
868
+ }),
869
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
870
+ variant: "outline",
871
+ onClick: () => onOpenChange(false),
872
+ children: tr("fluidgrids.dataTables.cancel", "Cancel")
873
+ }), /* @__PURE__ */ jsx(Button$1, {
874
+ onClick: handleSubmit,
875
+ disabled: saving || !table,
876
+ children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.saveRow", "Save row")
877
+ })] })
878
+ ] });
879
+ }
880
+
881
+ // src/modules/data-tables/utils/paging.ts
882
+ /** Page sizes offered in the rows pager; the first entry is the default. */
883
+ const ROW_PAGE_SIZES = [
884
+ 50,
885
+ 100,
886
+ 250
887
+ ];
888
+ const DEFAULT_ROW_PAGE_SIZE = ROW_PAGE_SIZES[0];
889
+ /** Number of pages needed for `total` rows; an empty table still has one page. */
890
+ function pageCount(total, pageSize) {
891
+ if (pageSize <= 0) return 1;
892
+ return Math.max(1, Math.ceil(total / pageSize));
893
+ }
894
+ /**
895
+ * Keep a zero-based page index inside the table. Needed after deleting rows,
896
+ * when the page being viewed may no longer exist.
897
+ */
898
+ function clampPage(page, total, pageSize) {
899
+ return Math.min(Math.max(page, 0), pageCount(total, pageSize) - 1);
900
+ }
901
+ /** One-based, inclusive `from`–`to` range of the rows shown on a page. */
902
+ function pageRange(page, pageSize, shown) {
903
+ if (shown <= 0) return {
904
+ from: 0,
905
+ to: 0
906
+ };
907
+ const from = page * pageSize + 1;
908
+ return {
909
+ from,
910
+ to: from + shown - 1
911
+ };
912
+ }
913
+
914
+ // src/modules/data-tables/components/RowsPanelHeaderV2.tsx
915
+ /**
916
+ * The top of the selected table's panel for Navigation v2 (BOFF-11849).
917
+ *
918
+ * Level 1: the table's name and description, its row count, and one action,
919
+ * "Add row". Level 2: a "⋯" menu (Edit table, Delete table, import and
920
+ * download as a spreadsheet, select several rows), "More options" (tags) and
921
+ * "Show technical details" (table ID with Copy, column count, size, row IDs,
922
+ * rows per page). Menu items a person has no permission for are left out.
923
+ */
924
+ function RowsPanelHeaderV2({ table, total, importProgress, exporting, deletingRows, deletingTable, selectMode, onSelectModeChange, selectedCount, showRowIds, onShowRowIdsChange, pageSize, onPageSizeChange, onAddRow, onEditTable, onDeleteTable, onImport, onExport, onDeleteSelected, onTagsChanged }) {
925
+ const tr = useTr();
926
+ const canTag = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE).allowed;
927
+ const canEditTable = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_UPDATE).allowed;
928
+ const canDeleteTable = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_DELETE).allowed;
929
+ const canAddRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE).allowed;
930
+ const canDeleteRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE).allowed;
931
+ const copyId = useCallback(async () => {
932
+ try {
933
+ await navigator.clipboard.writeText(table.id);
934
+ toast.success(tr("fluidgrids.dataTables.idCopied", "Table ID copied"));
935
+ } catch {
936
+ toast.error(tr("fluidgrids.dataTables.copyFailed", "Could not copy. Select the text and copy it yourself."));
937
+ }
938
+ }, [table.id, tr]);
939
+ const items = [];
940
+ if (canEditTable) items.push({
941
+ id: "edit-table",
942
+ label: tr("fluidgrids.dataTables.editTable", "Edit table"),
943
+ icon: "mdi:pencil-outline",
944
+ onSelect: onEditTable
945
+ });
946
+ if (canAddRows) items.push({
947
+ id: "import",
948
+ label: importProgress ? tr("fluidgrids.dataTables.importing", `Importing ${importProgress.done} / ${importProgress.total}…`) : tr("fluidgrids.dataTables.import", "Import from spreadsheet (CSV)"),
949
+ icon: "mdi:tray-arrow-up",
950
+ onSelect: onImport,
951
+ disabled: importProgress !== null
952
+ });
953
+ items.push({
954
+ id: "export",
955
+ label: exporting ? tr("fluidgrids.dataTables.exporting", "Exporting…") : tr("fluidgrids.dataTables.export", "Download as CSV"),
956
+ icon: "mdi:tray-arrow-down",
957
+ onSelect: onExport,
958
+ disabled: exporting
959
+ });
960
+ if (canDeleteTable) items.push({
961
+ id: "delete-table",
962
+ label: deletingTable ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteTable", "Delete table"),
963
+ icon: "mdi:trash-can-outline",
964
+ onSelect: onDeleteTable,
965
+ disabled: deletingTable,
966
+ destructive: true
967
+ });
968
+ const toggles = canDeleteRows ? [{
969
+ id: "select-rows",
970
+ label: tr("fluidgrids.dataTables.selectSeveral", "Select several rows"),
971
+ checked: selectMode,
972
+ onCheckedChange: onSelectModeChange
973
+ }] : [];
974
+ return /* @__PURE__ */ jsxs(CardHeader, {
975
+ className: "gap-4 border-b border-border-secondary p-4 sm:p-5",
976
+ children: [
977
+ /* @__PURE__ */ jsxs("div", {
978
+ className: "flex flex-wrap items-start justify-between gap-3",
979
+ children: [/* @__PURE__ */ jsxs("div", {
980
+ className: "min-w-0 flex-1 basis-64",
981
+ children: [/* @__PURE__ */ jsxs(CardTitle, {
982
+ className: "flex items-center gap-2 text-lg",
983
+ children: [/* @__PURE__ */ jsx(Table2, {
984
+ className: "size-5 shrink-0 text-text-secondary",
985
+ "aria-hidden": "true"
986
+ }), /* @__PURE__ */ jsx("span", {
987
+ className: "truncate",
988
+ children: table.name
989
+ })]
990
+ }), /* @__PURE__ */ jsx(CardDescription, {
991
+ className: "mt-1",
992
+ children: table.description || tr("fluidgrids.dataTables.noDescription", "No description")
993
+ })]
994
+ }), /* @__PURE__ */ jsxs("div", {
995
+ className: "flex shrink-0 items-center gap-2",
996
+ children: [/* @__PURE__ */ jsx(SdkPermissionButton, {
997
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
998
+ deniedTooltip: tr("fluidgrids.dataTables.addRowDeniedTooltip", "You don't have permission to add rows"),
999
+ children: /* @__PURE__ */ jsxs(Button$1, {
1000
+ size: "sm",
1001
+ className: "whitespace-nowrap",
1002
+ onClick: onAddRow,
1003
+ disabled: importProgress !== null,
1004
+ children: [/* @__PURE__ */ jsx(Plus, {
1005
+ className: "mr-1.5 size-4",
1006
+ "aria-hidden": "true"
1007
+ }), tr("fluidgrids.dataTables.addRow", "Add row")]
1008
+ })
1009
+ }), /* @__PURE__ */ jsx(RunActionsMenu, {
1010
+ items,
1011
+ toggles,
1012
+ label: tr("fluidgrids.dataTables.moreActions", "More table actions"),
1013
+ "data-testid": "table-actions-menu"
1014
+ })]
1015
+ })]
1016
+ }),
1017
+ /* @__PURE__ */ jsxs("div", {
1018
+ className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary",
1019
+ children: [/* @__PURE__ */ jsxs(Badge, {
1020
+ variant: "outline",
1021
+ children: [
1022
+ total,
1023
+ " ",
1024
+ tr("fluidgrids.dataTables.rows", "rows")
1025
+ ]
1026
+ }), selectMode && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1027
+ "aria-live": "polite",
1028
+ children: tr("fluidgrids.dataTables.selectedCount", "{{count}} selected", { count: selectedCount })
1029
+ }), selectedCount > 0 && canDeleteRows && /* @__PURE__ */ jsxs(Button$1, {
1030
+ variant: "outline",
1031
+ size: "sm",
1032
+ className: "whitespace-nowrap",
1033
+ onClick: onDeleteSelected,
1034
+ disabled: deletingRows,
1035
+ "data-testid": "delete-selected-rows",
1036
+ children: [/* @__PURE__ */ jsx(Trash2, {
1037
+ className: "mr-1.5 size-4",
1038
+ "aria-hidden": "true"
1039
+ }), deletingRows ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteSelected", `Delete ${selectedCount} selected`)]
1040
+ })] })]
1041
+ }),
1042
+ /* @__PURE__ */ jsxs("div", {
1043
+ className: "flex flex-col gap-2",
1044
+ children: [/* @__PURE__ */ jsx(Disclosure, {
1045
+ closedLabel: tr("fluidgrids.dataTables.moreOptions", "More options"),
1046
+ openLabel: tr("fluidgrids.dataTables.fewerOptions", "Fewer options"),
1047
+ "data-testid": "table-more-options",
1048
+ children: /* @__PURE__ */ jsx("div", {
1049
+ "data-testid": "data-table-tags",
1050
+ children: /* @__PURE__ */ jsx(EntityTagPicker, {
1051
+ entityType: "FLUIDGRIDS_DATA_TABLE",
1052
+ entityId: table.id,
1053
+ readOnly: !canTag,
1054
+ onChange: onTagsChanged
1055
+ })
1056
+ })
1057
+ }), /* @__PURE__ */ jsx(TechnicalDetails, {
1058
+ "data-testid": "table-technical-details",
1059
+ children: /* @__PURE__ */ jsxs("dl", {
1060
+ className: "grid gap-2 text-xs text-text-secondary sm:grid-cols-[auto_1fr] sm:items-center",
1061
+ children: [
1062
+ /* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.tableId", "Table ID") }),
1063
+ /* @__PURE__ */ jsxs("dd", {
1064
+ className: "flex flex-wrap items-center gap-2",
1065
+ children: [/* @__PURE__ */ jsx("code", {
1066
+ "data-testid": "table-id",
1067
+ children: table.id
1068
+ }), /* @__PURE__ */ jsx(Button$1, {
1069
+ variant: "outline",
1070
+ size: "sm",
1071
+ onClick: () => void copyId(),
1072
+ "data-testid": "table-id-copy",
1073
+ children: tr("fluidgrids.dataTables.copyId", "Copy")
1074
+ })]
1075
+ }),
1076
+ /* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.columns", "columns") }),
1077
+ /* @__PURE__ */ jsx("dd", { children: table.columns.length }),
1078
+ /* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.size", "Size") }),
1079
+ /* @__PURE__ */ jsx("dd", { children: formatBytes(table.sizeBytes) }),
1080
+ /* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.rowsPerPage", "Rows per page") }),
1081
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("select", {
1082
+ className: "h-8 rounded-md border border-input bg-background px-2 text-xs text-text-primary",
1083
+ value: pageSize,
1084
+ onChange: (event) => onPageSizeChange(Number(event.target.value)),
1085
+ "aria-label": tr("fluidgrids.dataTables.rowsPerPage", "Rows per page"),
1086
+ children: ROW_PAGE_SIZES.map((size) => /* @__PURE__ */ jsx("option", {
1087
+ value: size,
1088
+ children: size
1089
+ }, size))
1090
+ }) }),
1091
+ /* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.rowId", "Row") }),
1092
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsxs("label", {
1093
+ className: "flex items-center gap-2",
1094
+ children: [/* @__PURE__ */ jsx("input", {
1095
+ type: "checkbox",
1096
+ checked: showRowIds,
1097
+ onChange: (event) => onShowRowIdsChange(event.target.checked)
1098
+ }), tr("fluidgrids.dataTables.showRowIds", "Show row IDs in the table")]
1099
+ }) })
1100
+ ]
1101
+ })
1102
+ })]
1103
+ })
1104
+ ]
1105
+ });
1106
+ }
1107
+
1108
+ // src/modules/data-tables/utils/kinds.ts
1109
+ function kindLabel(tr, type) {
1110
+ switch (type) {
1111
+ case "STRING": return tr("fluidgrids.dataTables.kind.text", "Text");
1112
+ case "NUMBER": return tr("fluidgrids.dataTables.kind.number", "Number");
1113
+ case "BOOLEAN": return tr("fluidgrids.dataTables.kind.yesNo", "Yes / No");
1114
+ case "DATE": return tr("fluidgrids.dataTables.kind.date", "Date");
1115
+ case "JSON": return tr("fluidgrids.dataTables.kind.json", "JSON");
1116
+ }
1117
+ }
1118
+
1119
+ // src/modules/data-tables/components/TableNameFields.tsx
1120
+ /** The Name and Description fields shared by both table dialogs. */
1121
+ function TableNameFields({ name, description, onNameChange, onDescriptionChange }) {
1122
+ const tr = useTr();
1123
+ return /* @__PURE__ */ jsxs("div", {
1124
+ className: "grid gap-4 sm:grid-cols-2",
1125
+ children: [/* @__PURE__ */ jsxs("div", {
1126
+ className: "space-y-1.5",
1127
+ children: [/* @__PURE__ */ jsx(Label, {
1128
+ htmlFor: "data-table-name",
1129
+ children: tr("fluidgrids.dataTables.name", "Name")
1130
+ }), /* @__PURE__ */ jsx(Input, {
1131
+ id: "data-table-name",
1132
+ value: name,
1133
+ onChange: (event) => onNameChange(event.target.value),
1134
+ placeholder: "contacts",
1135
+ autoFocus: true
1136
+ })]
1137
+ }), /* @__PURE__ */ jsxs("div", {
1138
+ className: "space-y-1.5",
1139
+ children: [/* @__PURE__ */ jsx(Label, {
1140
+ htmlFor: "data-table-description",
1141
+ children: tr("fluidgrids.dataTables.description", "Description")
1142
+ }), /* @__PURE__ */ jsx(Input, {
1143
+ id: "data-table-description",
1144
+ value: description,
1145
+ onChange: (event) => onDescriptionChange(event.target.value),
1146
+ placeholder: tr("fluidgrids.dataTables.descriptionPlaceholder", "Optional")
1147
+ })]
1148
+ })]
1149
+ });
1150
+ }
1151
+
1152
+ // src/modules/data-tables/components/TableFormDialogV2.tsx
1153
+ /**
1154
+ * New / Edit table dialog for Navigation v2 (BOFF-11849).
1155
+ *
1156
+ * Plain on top: a column has a name and a kind of data (Text, Number, Yes / No,
1157
+ * Date). The name a workflow uses for the column is worked out from its name;
1158
+ * "Must be filled in", the starting value, saving as JSON and the raw name sit
1159
+ * under each column's "More options". Before a save that renames the table,
1160
+ * removes a column or changes a column's name-in-workflows or kind, a warning
1161
+ * says what could stop working.
1162
+ */
1163
+ /** The kinds people choose from; JSON is offered only to a column that already is JSON. */
1164
+ const EVERYDAY_KINDS = [
1165
+ "STRING",
1166
+ "NUMBER",
1167
+ "BOOLEAN",
1168
+ "DATE"
1169
+ ];
1170
+ function blankColumn(index) {
1171
+ return {
1172
+ key: `column_${index}`,
1173
+ label: `Column ${index}`,
1174
+ type: "STRING",
1175
+ required: false
1176
+ };
1177
+ }
1178
+ function withDefaultText(column) {
1179
+ return {
1180
+ ...column,
1181
+ defaultText: column.defaultValue === void 0 ? "" : asFormValue(column.defaultValue, column.type)
1182
+ };
1183
+ }
1184
+ function changeText(tr, change) {
1185
+ switch (change.kind) {
1186
+ case "renameTable": return tr("fluidgrids.dataTables.change.renameTable", "The table will be renamed from \"{{from}}\" to \"{{to}}\".", {
1187
+ from: change.from,
1188
+ to: change.to
1189
+ });
1190
+ case "removeColumn": return tr("fluidgrids.dataTables.change.removeColumn", "The column \"{{label}}\" will be removed, along with what is in it.", { label: change.label });
1191
+ case "changeKey": return tr("fluidgrids.dataTables.change.changeKey", "\"{{label}}\" will be used in workflows as \"{{to}}\" instead of \"{{from}}\".", {
1192
+ label: change.label,
1193
+ from: change.from,
1194
+ to: change.to
1195
+ });
1196
+ case "changeType": return tr("fluidgrids.dataTables.change.changeType", "\"{{label}}\" will change from {{from}} to {{to}}.", {
1197
+ label: change.label,
1198
+ from: kindLabel(tr, change.from),
1199
+ to: kindLabel(tr, change.to)
1200
+ });
1201
+ }
1202
+ }
1203
+ /**
1204
+ * The shell only mounts the form while the dialog is open, and the form is
1205
+ * keyed by the table being edited, so reopening always starts fresh.
1206
+ */
1207
+ function TableFormDialogV2({ open, table, onOpenChange, onSaved, workflowCount = null }) {
1208
+ return /* @__PURE__ */ jsx(Dialog, {
1209
+ open,
1210
+ onOpenChange,
1211
+ children: /* @__PURE__ */ jsx(DialogContent, {
1212
+ className: "max-h-[90vh] max-w-3xl overflow-y-auto",
1213
+ "data-testid": "table-form-dialog",
1214
+ children: /* @__PURE__ */ jsx(TableFormBody, {
1215
+ table,
1216
+ onOpenChange,
1217
+ onSaved,
1218
+ workflowCount
1219
+ }, table?.id ?? "new")
1220
+ })
1221
+ });
1222
+ }
1223
+ function TableFormBody({ table, onOpenChange, onSaved, workflowCount = null }) {
1224
+ const tr = useTr();
1225
+ const { createDataTable, loading: creating } = useCreateDataTable();
1226
+ const { updateDataTable, loading: updating } = useUpdateDataTable();
1227
+ const [name, setName] = useState(table?.name ?? "");
1228
+ const [description, setDescription] = useState(table?.description ?? "");
1229
+ const [columns, setColumns] = useState(() => toEditableColumns(table, blankColumn(1)).map(withDefaultText));
1230
+ const [pendingChanges, setPendingChanges] = useState(null);
1231
+ const editing = table !== null;
1232
+ const saving = creating || updating;
1233
+ const updateColumn = useCallback((uid, patch) => {
1234
+ setColumns((current) => current.map((column) => column.uid === uid ? {
1235
+ ...column,
1236
+ ...patch
1237
+ } : column));
1238
+ }, []);
1239
+ /** Typing a column name keeps its workflow name in step until that is edited by hand. */
1240
+ const renameColumn = useCallback((uid, label) => {
1241
+ setColumns((current) => current.map((column) => {
1242
+ if (column.uid !== uid) return column;
1243
+ if (column.keyEdited) return {
1244
+ ...column,
1245
+ label
1246
+ };
1247
+ const taken = new Set(current.filter((c) => c.uid !== uid).map((c) => c.key));
1248
+ const derived = label.trim() ? uniqueColumnKey(deriveColumnKey(label), taken) : "";
1249
+ return {
1250
+ ...column,
1251
+ label,
1252
+ key: derived
1253
+ };
1254
+ }));
1255
+ }, []);
1256
+ /** The columns as they will be saved, or a plain message why they cannot be. */
1257
+ const buildColumns = useCallback(() => {
1258
+ const resolved = [];
1259
+ const originals = new Map(table?.columns.map((c) => [c.key, c]));
1260
+ for (const column of columns) {
1261
+ const original = column.originalKey === void 0 ? void 0 : originals.get(column.originalKey);
1262
+ const text = (column.defaultText ?? "").trim();
1263
+ const unchanged = original !== void 0 && original.type === column.type && (column.defaultText ?? "") === (original.defaultValue === void 0 ? "" : asFormValue(original.defaultValue, original.type));
1264
+ let next = { ...column };
1265
+ if (!unchanged) {
1266
+ if (text === "") delete next.defaultValue;
1267
+ else try {
1268
+ next = {
1269
+ ...next,
1270
+ defaultValue: parseFormValue(text, column.type)
1271
+ };
1272
+ } catch (error) {
1273
+ return tr("fluidgrids.dataTables.startingValueInvalid", "The starting value for \"{{label}}\" is not valid: {{reason}}", {
1274
+ label: column.label.trim() || column.key,
1275
+ reason: error instanceof Error ? error.message : ""
1276
+ });
1277
+ }
1278
+ }
1279
+ resolved.push(next);
1280
+ }
1281
+ return resolved;
1282
+ }, [
1283
+ columns,
1284
+ table,
1285
+ tr
1286
+ ]);
1287
+ const save = useCallback(async (resolved) => {
1288
+ const input = {
1289
+ name: name.trim(),
1290
+ description: description.trim() || void 0,
1291
+ columns: toColumnInputs(resolved)
1292
+ };
1293
+ try {
1294
+ const saved = editing ? await updateDataTable(table.id, input) : await createDataTable(input);
1295
+ toast.success(editing ? tr("fluidgrids.dataTables.updated", "Data table updated") : tr("fluidgrids.dataTables.created", "Data table created"));
1296
+ setPendingChanges(null);
1297
+ onSaved(saved);
1298
+ onOpenChange(false);
1299
+ } catch (error) {
1300
+ setPendingChanges(null);
1301
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.saveFailed", "Failed to save data table")));
1302
+ }
1303
+ }, [
1304
+ createDataTable,
1305
+ description,
1306
+ editing,
1307
+ name,
1308
+ onOpenChange,
1309
+ onSaved,
1310
+ table,
1311
+ tr,
1312
+ updateDataTable
1313
+ ]);
1314
+ const handleSubmit = useCallback(async () => {
1315
+ if (!name.trim()) {
1316
+ toast.error(tr("fluidgrids.dataTables.nameRequired", "Table name is required"));
1317
+ return;
1318
+ }
1319
+ if (columns.length === 0 || columns.some((column) => !column.key.trim() || !column.label.trim())) {
1320
+ toast.error(tr("fluidgrids.dataTables.columnsRequired", "Add at least one column and give every column a name"));
1321
+ return;
1322
+ }
1323
+ if (new Set(columns.map((column) => column.key.trim())).size !== columns.length) {
1324
+ toast.error(tr("fluidgrids.dataTables.uniqueKeys", "Each column needs a different name."));
1325
+ return;
1326
+ }
1327
+ const resolved = buildColumns();
1328
+ if (typeof resolved === "string") {
1329
+ toast.error(resolved);
1330
+ return;
1331
+ }
1332
+ if (table) {
1333
+ const changes = describeSchemaChanges(table, {
1334
+ name,
1335
+ columns: resolved
1336
+ });
1337
+ if (changes.length > 0) {
1338
+ setPendingChanges(changes);
1339
+ return;
1340
+ }
1341
+ }
1342
+ await save(resolved);
1343
+ }, [
1344
+ buildColumns,
1345
+ columns,
1346
+ name,
1347
+ save,
1348
+ table,
1349
+ tr
1350
+ ]);
1351
+ const confirmChanges = useCallback(() => {
1352
+ const resolved = buildColumns();
1353
+ if (typeof resolved === "string") {
1354
+ setPendingChanges(null);
1355
+ toast.error(resolved);
1356
+ return;
1357
+ }
1358
+ save(resolved);
1359
+ }, [buildColumns, save]);
1360
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1361
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editTable", "Edit table") : tr("fluidgrids.dataTables.newTable", "New table") }) }),
1362
+ /* @__PURE__ */ jsxs("div", {
1363
+ className: "space-y-5 py-2",
1364
+ children: [/* @__PURE__ */ jsx(TableNameFields, {
1365
+ name,
1366
+ description,
1367
+ onNameChange: setName,
1368
+ onDescriptionChange: setDescription
1369
+ }), /* @__PURE__ */ jsxs("div", {
1370
+ className: "space-y-3",
1371
+ children: [/* @__PURE__ */ jsxs("div", {
1372
+ className: "flex items-center justify-between gap-3",
1373
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
1374
+ className: "text-sm font-semibold text-text-primary",
1375
+ children: tr("fluidgrids.dataTables.columns", "Columns")
1376
+ }), /* @__PURE__ */ jsx("p", {
1377
+ className: "text-xs text-text-secondary",
1378
+ children: tr("fluidgrids.dataTables.columnsHint", "Each column needs a different name.")
1379
+ })] }), /* @__PURE__ */ jsxs(Button$1, {
1380
+ type: "button",
1381
+ variant: "outline",
1382
+ size: "sm",
1383
+ onClick: () => setColumns((current) => {
1384
+ const taken = new Set(current.map((c) => c.key));
1385
+ const blank = blankColumn(current.length + 1);
1386
+ return [...current, withDefaultText({
1387
+ ...blank,
1388
+ key: uniqueColumnKey(blank.key, taken),
1389
+ uid: nextColumnUid()
1390
+ })];
1391
+ }),
1392
+ children: [/* @__PURE__ */ jsx(Plus, {
1393
+ className: "mr-1.5 size-4",
1394
+ "aria-hidden": "true"
1395
+ }), tr("fluidgrids.dataTables.addColumn", "Add column")]
1396
+ })]
1397
+ }), /* @__PURE__ */ jsx("div", {
1398
+ className: "space-y-2",
1399
+ children: columns.map((column, index) => /* @__PURE__ */ jsx(ColumnEditor, {
1400
+ column,
1401
+ index,
1402
+ canRemove: columns.length > 1,
1403
+ onRename: renameColumn,
1404
+ onChange: updateColumn,
1405
+ onRemove: (uid) => setColumns((current) => current.filter((c) => c.uid !== uid))
1406
+ }, column.uid))
1407
+ })]
1408
+ })]
1409
+ }),
1410
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
1411
+ variant: "outline",
1412
+ onClick: () => onOpenChange(false),
1413
+ children: tr("fluidgrids.dataTables.cancel", "Cancel")
1414
+ }), /* @__PURE__ */ jsx(Button$1, {
1415
+ onClick: handleSubmit,
1416
+ disabled: saving,
1417
+ children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.save", "Save table")
1418
+ })] }),
1419
+ /* @__PURE__ */ jsx(ConfirmActionDialog, {
1420
+ open: pendingChanges !== null,
1421
+ onOpenChange: (next) => {
1422
+ if (!next) setPendingChanges(null);
1423
+ },
1424
+ title: tr("fluidgrids.dataTables.schemaWarningTitle", "Save these changes to the table?"),
1425
+ description: /* @__PURE__ */ jsxs("div", {
1426
+ className: "space-y-2",
1427
+ "data-testid": "schema-change-warning",
1428
+ children: [/* @__PURE__ */ jsx("p", { children: workflowCount !== null ? tr("fluidgrids.dataTables.schemaWarningCount", "{{count}} workflow(s) use this table, and they may stop working after these changes.", { count: workflowCount }) : tr("fluidgrids.dataTables.schemaWarningIntro", "Workflows that use this table may stop working after these changes.") }), /* @__PURE__ */ jsx("ul", {
1429
+ className: "list-disc space-y-1 pl-5",
1430
+ children: (pendingChanges ?? []).map((change) => {
1431
+ const text = changeText(tr, change);
1432
+ return /* @__PURE__ */ jsx("li", { children: text }, text);
1433
+ })
1434
+ })]
1435
+ }),
1436
+ confirmLabel: tr("fluidgrids.dataTables.schemaWarningConfirm", "Save anyway"),
1437
+ cancelLabel: tr("fluidgrids.dataTables.schemaWarningBack", "Go back"),
1438
+ busy: saving,
1439
+ onConfirm: confirmChanges,
1440
+ "data-testid": "schema-change-dialog"
1441
+ })
1442
+ ] });
1443
+ }
1444
+ function ColumnEditor({ column, index, canRemove, onRename, onChange, onRemove }) {
1445
+ const tr = useTr();
1446
+ const kinds = column.type === "JSON" ? [...EVERYDAY_KINDS, "JSON"] : EVERYDAY_KINDS;
1447
+ return /* @__PURE__ */ jsxs("div", {
1448
+ className: "space-y-2 rounded-md border border-border-secondary p-3",
1449
+ "data-testid": "column-editor",
1450
+ children: [
1451
+ /* @__PURE__ */ jsxs("div", {
1452
+ className: "grid gap-2 sm:grid-cols-[1fr_10rem_auto] sm:items-end",
1453
+ children: [
1454
+ /* @__PURE__ */ jsxs("div", {
1455
+ className: "space-y-1",
1456
+ children: [/* @__PURE__ */ jsx(Label, {
1457
+ htmlFor: `data-table-column-label-${column.uid}`,
1458
+ children: tr("fluidgrids.dataTables.label", "Column name")
1459
+ }), /* @__PURE__ */ jsx(Input, {
1460
+ id: `data-table-column-label-${column.uid}`,
1461
+ value: column.label,
1462
+ onChange: (event) => onRename(column.uid, event.target.value)
1463
+ })]
1464
+ }),
1465
+ /* @__PURE__ */ jsxs("div", {
1466
+ className: "space-y-1",
1467
+ children: [/* @__PURE__ */ jsx(Label, {
1468
+ htmlFor: `data-table-column-type-${column.uid}`,
1469
+ children: tr("fluidgrids.dataTables.type", "Kind of data")
1470
+ }), /* @__PURE__ */ jsx("select", {
1471
+ id: `data-table-column-type-${column.uid}`,
1472
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-text-primary",
1473
+ value: column.type,
1474
+ onChange: (event) => onChange(column.uid, { type: event.target.value }),
1475
+ children: kinds.map((type) => /* @__PURE__ */ jsx("option", {
1476
+ value: type,
1477
+ children: kindLabel(tr, type)
1478
+ }, type))
1479
+ })]
1480
+ }),
1481
+ /* @__PURE__ */ jsx("button", {
1482
+ type: "button",
1483
+ className: "justify-self-end rounded p-2 text-text-secondary hover:bg-background-tertiary hover:text-text-primary disabled:opacity-40",
1484
+ onClick: () => onRemove(column.uid),
1485
+ disabled: !canRemove,
1486
+ title: tr("fluidgrids.dataTables.removeColumn", "Remove column"),
1487
+ "aria-label": `${tr("fluidgrids.dataTables.removeColumn", "Remove column")} ${index + 1}`,
1488
+ children: /* @__PURE__ */ jsx(Trash2, {
1489
+ className: "size-4",
1490
+ "aria-hidden": "true"
1491
+ })
1492
+ })
1493
+ ]
1494
+ }),
1495
+ /* @__PURE__ */ jsxs("p", {
1496
+ className: "text-xs text-text-secondary",
1497
+ "data-testid": "column-key-hint",
1498
+ children: [
1499
+ tr("fluidgrids.dataTables.key", "Used in workflows as"),
1500
+ ":",
1501
+ " ",
1502
+ /* @__PURE__ */ jsx("code", { children: column.key || "—" })
1503
+ ]
1504
+ }),
1505
+ /* @__PURE__ */ jsx(Disclosure, {
1506
+ closedLabel: tr("fluidgrids.dataTables.moreOptions", "More options"),
1507
+ openLabel: tr("fluidgrids.dataTables.fewerOptions", "Fewer options"),
1508
+ defaultOpen: column.required === true || column.type === "JSON" || (column.defaultText ?? "") !== "",
1509
+ children: /* @__PURE__ */ jsxs("div", {
1510
+ className: "grid gap-3 sm:grid-cols-2",
1511
+ children: [
1512
+ /* @__PURE__ */ jsxs("div", {
1513
+ className: "space-y-1",
1514
+ children: [/* @__PURE__ */ jsx(Label, {
1515
+ htmlFor: `data-table-column-key-${column.uid}`,
1516
+ children: tr("fluidgrids.dataTables.key", "Used in workflows as")
1517
+ }), /* @__PURE__ */ jsx(Input, {
1518
+ id: `data-table-column-key-${column.uid}`,
1519
+ value: column.key,
1520
+ onChange: (event) => onChange(column.uid, {
1521
+ key: event.target.value,
1522
+ keyEdited: true
1523
+ })
1524
+ })]
1525
+ }),
1526
+ /* @__PURE__ */ jsxs("div", {
1527
+ className: "space-y-1",
1528
+ children: [/* @__PURE__ */ jsx(Label, {
1529
+ htmlFor: `data-table-column-default-${column.uid}`,
1530
+ children: tr("fluidgrids.dataTables.defaultValue", "Starting value")
1531
+ }), /* @__PURE__ */ jsx(Input, {
1532
+ id: `data-table-column-default-${column.uid}`,
1533
+ value: column.defaultText ?? "",
1534
+ onChange: (event) => onChange(column.uid, { defaultText: event.target.value }),
1535
+ placeholder: tr("fluidgrids.dataTables.defaultValueHint", "Filled in when you add a row")
1536
+ })]
1537
+ }),
1538
+ /* @__PURE__ */ jsxs("label", {
1539
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1540
+ children: [/* @__PURE__ */ jsx("input", {
1541
+ type: "checkbox",
1542
+ checked: column.required ?? false,
1543
+ onChange: (event) => onChange(column.uid, { required: event.target.checked })
1544
+ }), tr("fluidgrids.dataTables.required", "Must be filled in")]
1545
+ }),
1546
+ /* @__PURE__ */ jsxs("label", {
1547
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1548
+ children: [/* @__PURE__ */ jsx("input", {
1549
+ type: "checkbox",
1550
+ checked: column.type === "JSON",
1551
+ onChange: (event) => onChange(column.uid, { type: event.target.checked ? "JSON" : "STRING" })
1552
+ }), tr("fluidgrids.dataTables.saveAsJson", "Save values as JSON (for nested data)")]
1553
+ })
1554
+ ]
1555
+ })
1556
+ })
1557
+ ]
1558
+ });
1559
+ }
1560
+
1561
+ // src/modules/data-tables/pages/DataTablesPage.tsx
1562
+ var DataTablesPage_exports = /* @__PURE__ */ __exportAll({ DataTablesPage: () => DataTablesPage });
1563
+ const COLUMN_TYPES = [
1564
+ "STRING",
1565
+ "NUMBER",
1566
+ "BOOLEAN",
1567
+ "DATE",
1568
+ "JSON"
1569
+ ];
1570
+ function emptyColumn(index) {
1571
+ return {
1572
+ key: `column_${index}`,
1573
+ label: `Column ${index}`,
1574
+ type: "STRING",
1575
+ required: false
1576
+ };
1577
+ }
1578
+ function columnTypeLabel(type) {
1579
+ return type === "JSON" ? "JSON" : type[0] + type.slice(1).toLowerCase();
1580
+ }
1581
+ function TableFormDialog({ open, table, onOpenChange, onSaved }) {
1582
+ const tr = useTr();
1583
+ const { createDataTable, loading: creating } = useCreateDataTable();
1584
+ const { updateDataTable, loading: updating } = useUpdateDataTable();
1585
+ const [name, setName] = useState("");
1586
+ const [description, setDescription] = useState("");
1587
+ const [columns, setColumns] = useState([]);
1588
+ const editing = table !== null;
1589
+ const saving = creating || updating;
1590
+ useEffect(() => {
1591
+ if (!open) return;
1592
+ setName(table?.name ?? "");
1593
+ setDescription(table?.description ?? "");
1594
+ setColumns(toEditableColumns(table, emptyColumn(1)));
1595
+ }, [open, table]);
1596
+ const updateColumn = useCallback((index, patch) => {
1597
+ setColumns((current) => current.map((column, columnIndex) => columnIndex === index ? {
1598
+ ...column,
1599
+ ...patch
1600
+ } : column));
1601
+ }, []);
1602
+ const handleSubmit = useCallback(async () => {
1603
+ const trimmedName = name.trim();
1604
+ if (!trimmedName) {
1605
+ toast.error(tr("fluidgrids.dataTables.nameRequired", "Table name is required"));
1606
+ return;
1607
+ }
1608
+ if (columns.length === 0 || columns.some((column) => !column.key.trim() || !column.label.trim())) {
1609
+ toast.error(tr("fluidgrids.dataTables.columnsRequired", "Add at least one column with a key and label"));
1610
+ return;
1611
+ }
1612
+ const normalizedColumns = toColumnInputs(columns);
1613
+ if (new Set(normalizedColumns.map((column) => column.key)).size !== normalizedColumns.length) {
1614
+ toast.error(tr("fluidgrids.dataTables.uniqueKeys", "Column keys must be unique"));
1615
+ return;
1616
+ }
1617
+ try {
1618
+ const saved = editing ? await updateDataTable(table.id, {
1619
+ name: trimmedName,
1620
+ description: description.trim() || void 0,
1621
+ columns: normalizedColumns
1622
+ }) : await createDataTable({
1623
+ name: trimmedName,
1624
+ description: description.trim() || void 0,
1625
+ columns: normalizedColumns
1626
+ });
1627
+ toast.success(editing ? tr("fluidgrids.dataTables.updated", "Data table updated") : tr("fluidgrids.dataTables.created", "Data table created"));
1628
+ onSaved(saved);
1629
+ onOpenChange(false);
1630
+ } catch (error) {
1631
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.saveFailed", "Failed to save data table")));
1632
+ }
1633
+ }, [
1634
+ columns,
1635
+ createDataTable,
1636
+ description,
1637
+ editing,
1638
+ name,
1639
+ onOpenChange,
1640
+ onSaved,
1641
+ table,
1642
+ tr,
1643
+ updateDataTable
1644
+ ]);
1645
+ return /* @__PURE__ */ jsx(Dialog, {
1646
+ open,
1647
+ onOpenChange,
1648
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1649
+ className: "max-h-[90vh] max-w-3xl overflow-y-auto",
1650
+ children: [
1651
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editTable", "Edit data table") : tr("fluidgrids.dataTables.newTable", "New data table") }) }),
1652
+ /* @__PURE__ */ jsxs("div", {
1653
+ className: "space-y-5 py-2",
1654
+ children: [/* @__PURE__ */ jsx(TableNameFields, {
1655
+ name,
1656
+ description,
1657
+ onNameChange: setName,
1658
+ onDescriptionChange: setDescription
1659
+ }), /* @__PURE__ */ jsxs("div", {
1660
+ className: "space-y-3",
1661
+ children: [/* @__PURE__ */ jsxs("div", {
1662
+ className: "flex items-center justify-between gap-3",
1663
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
1664
+ className: "text-sm font-semibold text-text-primary",
1665
+ children: tr("fluidgrids.dataTables.columns", "Columns")
1666
+ }), /* @__PURE__ */ jsx("p", {
1667
+ className: "text-xs text-text-secondary",
1668
+ children: tr("fluidgrids.dataTables.columnsHint", "Column keys are used by workflow nodes and must be unique.")
1669
+ })] }), /* @__PURE__ */ jsxs(Button$1, {
1670
+ type: "button",
1671
+ variant: "outline",
1672
+ size: "sm",
1673
+ onClick: () => setColumns((current) => [...current, {
1674
+ ...emptyColumn(current.length + 1),
1675
+ uid: nextColumnUid()
1676
+ }]),
1677
+ children: [/* @__PURE__ */ jsx(Plus, {
1678
+ className: "mr-1.5 size-4",
1679
+ "aria-hidden": "true"
1680
+ }), tr("fluidgrids.dataTables.addColumn", "Add column")]
1681
+ })]
1682
+ }), /* @__PURE__ */ jsx("div", {
1683
+ className: "space-y-2",
1684
+ children: columns.map((column, index) => /* @__PURE__ */ jsxs("div", {
1685
+ className: "grid gap-2 rounded-md border border-border-secondary p-3 sm:grid-cols-[1fr_1fr_8rem_auto] sm:items-end",
1686
+ children: [
1687
+ /* @__PURE__ */ jsxs("div", {
1688
+ className: "space-y-1",
1689
+ children: [/* @__PURE__ */ jsx(Label, {
1690
+ htmlFor: `data-table-column-key-${index}`,
1691
+ children: tr("fluidgrids.dataTables.key", "Key")
1692
+ }), /* @__PURE__ */ jsx(Input, {
1693
+ id: `data-table-column-key-${index}`,
1694
+ value: column.key,
1695
+ onChange: (event) => updateColumn(index, { key: event.target.value })
1696
+ })]
1697
+ }),
1698
+ /* @__PURE__ */ jsxs("div", {
1699
+ className: "space-y-1",
1700
+ children: [/* @__PURE__ */ jsx(Label, {
1701
+ htmlFor: `data-table-column-label-${index}`,
1702
+ children: tr("fluidgrids.dataTables.label", "Label")
1703
+ }), /* @__PURE__ */ jsx(Input, {
1704
+ id: `data-table-column-label-${index}`,
1705
+ value: column.label,
1706
+ onChange: (event) => updateColumn(index, { label: event.target.value })
1707
+ })]
1708
+ }),
1709
+ /* @__PURE__ */ jsxs("div", {
1710
+ className: "space-y-1",
1711
+ children: [/* @__PURE__ */ jsx(Label, {
1712
+ htmlFor: `data-table-column-type-${index}`,
1713
+ children: tr("fluidgrids.dataTables.type", "Type")
1714
+ }), /* @__PURE__ */ jsx("select", {
1715
+ id: `data-table-column-type-${index}`,
1716
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-text-primary",
1717
+ value: column.type,
1718
+ onChange: (event) => updateColumn(index, { type: event.target.value }),
1719
+ children: COLUMN_TYPES.map((type) => /* @__PURE__ */ jsx("option", {
1720
+ value: type,
1721
+ children: columnTypeLabel(type)
1722
+ }, type))
1723
+ })]
1724
+ }),
1725
+ /* @__PURE__ */ jsxs("div", {
1726
+ className: "flex items-center gap-2 pb-2 sm:flex-col sm:items-center sm:gap-1",
1727
+ children: [/* @__PURE__ */ jsxs("label", {
1728
+ className: "flex items-center gap-2 text-xs text-text-secondary",
1729
+ children: [/* @__PURE__ */ jsx("input", {
1730
+ type: "checkbox",
1731
+ checked: column.required ?? false,
1732
+ onChange: (event) => updateColumn(index, { required: event.target.checked })
1733
+ }), tr("fluidgrids.dataTables.required", "Required")]
1734
+ }), /* @__PURE__ */ jsx("button", {
1735
+ type: "button",
1736
+ className: "rounded p-1 text-text-secondary hover:bg-background-tertiary hover:text-text-primary disabled:opacity-40",
1737
+ onClick: () => setColumns((current) => current.filter((_, columnIndex) => columnIndex !== index)),
1738
+ disabled: columns.length === 1,
1739
+ title: tr("fluidgrids.dataTables.removeColumn", "Remove column"),
1740
+ "aria-label": tr("fluidgrids.dataTables.removeColumn", "Remove column"),
1741
+ children: /* @__PURE__ */ jsx(Trash2, {
1742
+ className: "size-4",
1743
+ "aria-hidden": "true"
1744
+ })
1745
+ })]
1746
+ })
1747
+ ]
1748
+ }, column.uid))
1749
+ })]
1750
+ })]
1751
+ }),
1752
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
1753
+ variant: "outline",
1754
+ onClick: () => onOpenChange(false),
1755
+ children: tr("fluidgrids.dataTables.cancel", "Cancel")
1756
+ }), /* @__PURE__ */ jsx(Button$1, {
1757
+ onClick: handleSubmit,
1758
+ disabled: saving,
1759
+ children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.save", "Save table")
1760
+ })] })
1761
+ ]
1762
+ })
1763
+ });
1764
+ }
1765
+ function RowFormDialog({ open, table, row, onOpenChange, onSaved }) {
1766
+ const tr = useTr();
1767
+ const { insertDataTableRow, loading: inserting } = useInsertDataTableRow();
1768
+ const { updateDataTableRow, loading: updating } = useUpdateDataTableRow();
1769
+ const [values, setValues] = useState({});
1770
+ const editing = row !== null;
1771
+ const saving = inserting || updating;
1772
+ useEffect(() => {
1773
+ if (!open || !table) return;
1774
+ const next = {};
1775
+ for (const column of table.columns) next[column.key] = asFormValue(row?.data[column.key], column.type);
1776
+ setValues(next);
1777
+ }, [
1778
+ open,
1779
+ row,
1780
+ table
1781
+ ]);
1782
+ const handleSubmit = useCallback(async () => {
1783
+ if (!table) return;
1784
+ try {
1785
+ const data = {};
1786
+ for (const column of table.columns) {
1787
+ const value = values[column.key] ?? "";
1788
+ if (column.required && value.trim() === "") throw new Error(`${column.label} is required`);
1789
+ data[column.key] = parseFormValue(value, column.type);
1790
+ }
1791
+ if (editing && row) await updateDataTableRow(row.id, data);
1792
+ else await insertDataTableRow(table.id, data);
1793
+ toast.success(editing ? tr("fluidgrids.dataTables.rowUpdated", "Row updated") : tr("fluidgrids.dataTables.rowCreated", "Row added"));
1794
+ onSaved();
1795
+ onOpenChange(false);
1796
+ } catch (error) {
1797
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.rowSaveFailed", "Failed to save row")));
1798
+ }
1799
+ }, [
1800
+ editing,
1801
+ insertDataTableRow,
1802
+ onOpenChange,
1803
+ onSaved,
1804
+ row,
1805
+ table,
1806
+ tr,
1807
+ updateDataTableRow,
1808
+ values
1809
+ ]);
1810
+ return /* @__PURE__ */ jsx(Dialog, {
1811
+ open,
1812
+ onOpenChange,
1813
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1814
+ className: "max-h-[90vh] max-w-2xl overflow-y-auto",
1815
+ children: [
1816
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editRow", "Edit row") : tr("fluidgrids.dataTables.addRow", "Add row") }) }),
1817
+ table && /* @__PURE__ */ jsx("div", {
1818
+ className: "space-y-4 py-2",
1819
+ children: table.columns.map((column) => /* @__PURE__ */ jsxs("div", {
1820
+ className: "space-y-1.5",
1821
+ children: [/* @__PURE__ */ jsxs(Label, {
1822
+ htmlFor: `data-table-row-${column.key}`,
1823
+ children: [
1824
+ column.label,
1825
+ /* @__PURE__ */ jsxs("span", {
1826
+ className: "ml-1 text-xs text-text-secondary",
1827
+ children: [
1828
+ "(",
1829
+ column.key,
1830
+ " · ",
1831
+ columnTypeLabel(column.type),
1832
+ ")"
1833
+ ]
1834
+ }),
1835
+ column.required && /* @__PURE__ */ jsx("span", {
1836
+ className: "ml-1 text-text-secondary",
1837
+ children: "*"
1838
+ })
1839
+ ]
1840
+ }), column.type === "JSON" ? /* @__PURE__ */ jsx(Textarea, {
1841
+ id: `data-table-row-${column.key}`,
1842
+ value: values[column.key] ?? "",
1843
+ onChange: (event) => setValues((current) => ({
1844
+ ...current,
1845
+ [column.key]: event.target.value
1846
+ })),
1847
+ rows: 4,
1848
+ placeholder: "{ }"
1849
+ }) : /* @__PURE__ */ jsx(Input, {
1850
+ id: `data-table-row-${column.key}`,
1851
+ type: column.type === "NUMBER" ? "number" : column.type === "DATE" ? "datetime-local" : "text",
1852
+ value: values[column.key] ?? "",
1853
+ onChange: (event) => setValues((current) => ({
1854
+ ...current,
1855
+ [column.key]: event.target.value
1856
+ }))
1857
+ })]
1858
+ }, column.key))
1859
+ }),
1860
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
1861
+ variant: "outline",
1862
+ onClick: () => onOpenChange(false),
1863
+ children: tr("fluidgrids.dataTables.cancel", "Cancel")
1864
+ }), /* @__PURE__ */ jsx(Button$1, {
1865
+ onClick: handleSubmit,
1866
+ disabled: saving || !table,
1867
+ children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.saveRow", "Save row")
1868
+ })] })
1869
+ ]
1870
+ })
1871
+ });
1872
+ }
1873
+ function TableRowsPanel({ table, onTableChanged, onEditTable, onDeleteTable, deletingTable, onTagsChanged }) {
1874
+ const tr = useTr();
1875
+ const v2 = useNavigationV2();
1876
+ const { allowed: canTag } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
1877
+ const canEditRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_UPDATE).allowed;
1878
+ const canDeleteRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE).allowed;
1879
+ const [page, setPage] = useState(0);
1880
+ const [pageSize, setPageSize] = useState(DEFAULT_ROW_PAGE_SIZE);
1881
+ const { rows, total, loading, error, refetch } = useDataTableRows(table.id, {
1882
+ page,
1883
+ pageSize
1884
+ });
1885
+ const { deleteDataTableRows, loading: deleting } = useDeleteDataTableRows();
1886
+ const { insertDataTableRow } = useInsertDataTableRow();
1887
+ const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
1888
+ const [rowDialogOpen, setRowDialogOpen] = useState(false);
1889
+ const [editingRow, setEditingRow] = useState(null);
1890
+ const [exporting, setExporting] = useState(false);
1891
+ const [selectMode, setSelectMode] = useState(false);
1892
+ const [showRowIds, setShowRowIds] = useState(false);
1893
+ const [rowSearch, setRowSearch] = useState("");
1894
+ const [sort, setSort] = useState(null);
1895
+ const [pendingRowDelete, setPendingRowDelete] = useState(null);
1896
+ const [importProgress, setImportProgress] = useState(null);
1897
+ const fileInputRef = useRef(null);
1898
+ const pages = pageCount(total, pageSize);
1899
+ const range = pageRange(page, pageSize, rows.length);
1900
+ useEffect(() => {
1901
+ if (loading) return;
1902
+ const clamped = clampPage(page, total, pageSize);
1903
+ if (clamped !== page) setPage(clamped);
1904
+ }, [
1905
+ loading,
1906
+ page,
1907
+ pageSize,
1908
+ total
1909
+ ]);
1910
+ useEffect(() => {
1911
+ setSelectedIds((current) => {
1912
+ const visible = new Set(rows.map((row) => row.id));
1913
+ return new Set([...current].filter((id) => visible.has(id)));
1914
+ });
1915
+ }, [rows]);
1916
+ const allVisibleSelected = rows.length > 0 && selectedIds.size === rows.length;
1917
+ const toggleAll = useCallback(() => {
1918
+ setSelectedIds(allVisibleSelected ? /* @__PURE__ */ new Set() : new Set(rows.map((row) => row.id)));
1919
+ }, [allVisibleSelected, rows]);
1920
+ const performDeleteRows = useCallback(async (ids) => {
1921
+ try {
1922
+ const affected = await deleteDataTableRows(table.id, ids);
1923
+ toast.success(tr("fluidgrids.dataTables.rowsDeleted", `${affected} row(s) deleted`));
1924
+ setSelectedIds(/* @__PURE__ */ new Set());
1925
+ await refetch();
1926
+ onTableChanged();
1927
+ } catch (error) {
1928
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.deleteRowsFailed", "Failed to delete rows")));
1929
+ }
1930
+ }, [
1931
+ deleteDataTableRows,
1932
+ onTableChanged,
1933
+ refetch,
1934
+ table.id,
1935
+ tr
1936
+ ]);
1937
+ const handleDeleteRows = useCallback(async () => {
1938
+ if (selectedIds.size === 0) return;
1939
+ if (v2) {
1940
+ setPendingRowDelete([...selectedIds]);
1941
+ return;
1942
+ }
1943
+ if (!window.confirm(tr("fluidgrids.dataTables.confirmDeleteRows", `Delete ${selectedIds.size} selected row(s)?`))) return;
1944
+ await performDeleteRows([...selectedIds]);
1945
+ }, [
1946
+ performDeleteRows,
1947
+ selectedIds,
1948
+ tr,
1949
+ v2
1950
+ ]);
1951
+ const confirmRowDelete = useCallback(async () => {
1952
+ const ids = pendingRowDelete;
1953
+ if (!ids) return;
1954
+ await performDeleteRows(ids);
1955
+ setPendingRowDelete(null);
1956
+ }, [pendingRowDelete, performDeleteRows]);
1957
+ const handleExport = useCallback(async () => {
1958
+ setExporting(true);
1959
+ try {
1960
+ const allRows = await fetchAllDataTableRows(table.id);
1961
+ const csv = rowsToCsv(table.columns, allRows);
1962
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
1963
+ const url = URL.createObjectURL(blob);
1964
+ const anchor = document.createElement("a");
1965
+ anchor.href = url;
1966
+ anchor.download = `${table.name}.csv`;
1967
+ anchor.click();
1968
+ URL.revokeObjectURL(url);
1969
+ toast.success(tr("fluidgrids.dataTables.exported", `${allRows.length} row(s) exported to CSV`));
1970
+ } catch (error) {
1971
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.exportFailed", "Failed to export CSV")));
1972
+ } finally {
1973
+ setExporting(false);
1974
+ }
1975
+ }, [
1976
+ table.columns,
1977
+ table.id,
1978
+ table.name,
1979
+ tr
1980
+ ]);
1981
+ const handleImport = useCallback(async (event) => {
1982
+ const file = event.target.files?.[0];
1983
+ event.target.value = "";
1984
+ if (!file) return;
1985
+ let inserted = 0;
1986
+ const insertedIds = [];
1987
+ try {
1988
+ const importedRows = csvToRows(await file.text(), table.columns);
1989
+ setImportProgress({
1990
+ done: 0,
1991
+ total: importedRows.length
1992
+ });
1993
+ for (const data of importedRows) {
1994
+ const added = await insertDataTableRow(table.id, data);
1995
+ if (added) insertedIds.push(added.id);
1996
+ inserted += 1;
1997
+ setImportProgress({
1998
+ done: inserted,
1999
+ total: importedRows.length
2000
+ });
2001
+ }
2002
+ toast.success(tr("fluidgrids.dataTables.imported", `${inserted} row(s) imported`));
2003
+ } catch (error) {
2004
+ const reason = describeOperationError(error, tr("fluidgrids.dataTables.importFailed", "Failed to import CSV"));
2005
+ const undone = inserted > 0 && insertedIds.length === inserted ? await deleteDataTableRows(table.id, insertedIds).then(() => true, () => false) : false;
2006
+ toast.error(inserted === 0 ? reason : undone ? tr("fluidgrids.dataTables.importRolledBack", "Import failed, so none of its rows were added: {{reason}}", { reason }) : tr("fluidgrids.dataTables.importPartial", `Import stopped after ${inserted} row(s): ${reason}`));
2007
+ } finally {
2008
+ setImportProgress(null);
2009
+ setPage(0);
2010
+ await refetch();
2011
+ onTableChanged();
2012
+ }
2013
+ }, [
2014
+ deleteDataTableRows,
2015
+ insertDataTableRow,
2016
+ onTableChanged,
2017
+ refetch,
2018
+ table.columns,
2019
+ table.id,
2020
+ tr
2021
+ ]);
2022
+ const bodyColumnCount = table.columns.length + 1 + Number(!v2 || selectMode) + Number(!v2 || showRowIds);
2023
+ const yesNo = {
2024
+ yes: tr("fluidgrids.dataTables.yes", "Yes"),
2025
+ no: tr("fluidgrids.dataTables.no", "No")
2026
+ };
2027
+ const displayRows = useMemo(() => {
2028
+ if (!v2) return rows;
2029
+ const needle = rowSearch.trim().toLowerCase();
2030
+ const matching = needle ? rows.filter((row) => table.columns.some((column) => formatValue(row.data[column.key]).toLowerCase().includes(needle))) : rows;
2031
+ const sortColumn = sort ? table.columns.find((column) => column.key === sort.key) : void 0;
2032
+ if (!sort || !sortColumn) return matching;
2033
+ return [...matching].sort((a, b) => compareCells(a.data[sortColumn.key], b.data[sortColumn.key], sortColumn.type, sort.direction));
2034
+ }, [
2035
+ rowSearch,
2036
+ rows,
2037
+ sort,
2038
+ table.columns,
2039
+ v2
2040
+ ]);
2041
+ const toggleSort = useCallback((key) => {
2042
+ setSort((current) => {
2043
+ if (!current || current.key !== key) return {
2044
+ key,
2045
+ direction: 1
2046
+ };
2047
+ return current.direction === 1 ? {
2048
+ key,
2049
+ direction: -1
2050
+ } : null;
2051
+ });
2052
+ }, []);
2053
+ const openNewRow = useCallback(() => {
2054
+ setEditingRow(null);
2055
+ setRowDialogOpen(true);
2056
+ }, []);
2057
+ const openEditRow = useCallback((row) => {
2058
+ setEditingRow(row);
2059
+ setRowDialogOpen(true);
2060
+ }, []);
2061
+ return /* @__PURE__ */ jsxs(Card, {
2062
+ className: "min-w-0 border-border-secondary",
2063
+ children: [
2064
+ v2 && /* @__PURE__ */ jsx("input", {
2065
+ ref: fileInputRef,
2066
+ type: "file",
2067
+ accept: ".csv,text/csv",
2068
+ className: "hidden",
2069
+ "aria-label": tr("fluidgrids.dataTables.import", "Import from spreadsheet (CSV)"),
2070
+ onChange: handleImport
2071
+ }),
2072
+ v2 ? /* @__PURE__ */ jsx(RowsPanelHeaderV2, {
2073
+ table,
2074
+ total,
2075
+ importProgress,
2076
+ exporting,
2077
+ deletingRows: deleting,
2078
+ deletingTable,
2079
+ selectMode,
2080
+ onSelectModeChange: (on) => {
2081
+ setSelectMode(on);
2082
+ if (!on) setSelectedIds(/* @__PURE__ */ new Set());
2083
+ },
2084
+ selectedCount: selectedIds.size,
2085
+ showRowIds,
2086
+ onShowRowIdsChange: setShowRowIds,
2087
+ pageSize,
2088
+ onPageSizeChange: (size) => {
2089
+ setPageSize(size);
2090
+ setPage(0);
2091
+ },
2092
+ onAddRow: openNewRow,
2093
+ onEditTable,
2094
+ onDeleteTable,
2095
+ onImport: () => fileInputRef.current?.click(),
2096
+ onExport: handleExport,
2097
+ onDeleteSelected: handleDeleteRows,
2098
+ onTagsChanged
2099
+ }) : /* @__PURE__ */ jsxs(CardHeader, {
2100
+ className: "gap-4 border-b border-border-secondary p-4 sm:p-5",
2101
+ children: [/* @__PURE__ */ jsxs("div", {
2102
+ className: "flex flex-wrap items-start justify-between gap-3",
2103
+ children: [/* @__PURE__ */ jsxs("div", {
2104
+ className: "min-w-0 flex-1 basis-64",
2105
+ children: [
2106
+ /* @__PURE__ */ jsxs(CardTitle, {
2107
+ className: "flex items-center gap-2 text-lg",
2108
+ children: [/* @__PURE__ */ jsx(Table2, {
2109
+ className: "size-5 shrink-0 text-text-secondary",
2110
+ "aria-hidden": "true"
2111
+ }), /* @__PURE__ */ jsx("span", {
2112
+ className: "truncate",
2113
+ children: table.name
2114
+ })]
2115
+ }),
2116
+ /* @__PURE__ */ jsx(CardDescription, {
2117
+ className: "mt-1",
2118
+ children: table.description || tr("fluidgrids.dataTables.noDescription", "No description")
2119
+ }),
2120
+ /* @__PURE__ */ jsx("div", {
2121
+ className: "mt-3",
2122
+ "data-testid": "data-table-tags",
2123
+ children: /* @__PURE__ */ jsx(EntityTagPicker, {
2124
+ entityType: "FLUIDGRIDS_DATA_TABLE",
2125
+ entityId: table.id,
2126
+ readOnly: !canTag,
2127
+ onChange: onTagsChanged
2128
+ })
2129
+ })
2130
+ ]
2131
+ }), /* @__PURE__ */ jsxs("div", {
2132
+ className: "flex shrink-0 items-center gap-2",
2133
+ children: [/* @__PURE__ */ jsx(SdkPermissionButton, {
2134
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_UPDATE,
2135
+ deniedTooltip: tr("fluidgrids.dataTables.editDeniedTooltip", "You don't have permission to edit data tables"),
2136
+ children: /* @__PURE__ */ jsxs(Button$1, {
2137
+ variant: "outline",
2138
+ size: "sm",
2139
+ className: "whitespace-nowrap",
2140
+ onClick: onEditTable,
2141
+ children: [/* @__PURE__ */ jsx(Pencil, {
2142
+ className: "mr-1.5 size-4",
2143
+ "aria-hidden": "true"
2144
+ }), tr("fluidgrids.dataTables.editTable", "Edit table")]
2145
+ })
2146
+ }), /* @__PURE__ */ jsx(SdkPermissionButton, {
2147
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_DELETE,
2148
+ deniedTooltip: tr("fluidgrids.dataTables.deleteDeniedTooltip", "You don't have permission to delete data tables"),
2149
+ children: /* @__PURE__ */ jsxs(Button$1, {
2150
+ variant: "outline",
2151
+ size: "sm",
2152
+ className: "whitespace-nowrap",
2153
+ onClick: onDeleteTable,
2154
+ disabled: deletingTable,
2155
+ children: [/* @__PURE__ */ jsx(Trash2, {
2156
+ className: "mr-1.5 size-4",
2157
+ "aria-hidden": "true"
2158
+ }), deletingTable ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteTable", "Delete table")]
2159
+ })
2160
+ })]
2161
+ })]
2162
+ }), /* @__PURE__ */ jsxs("div", {
2163
+ className: "flex flex-wrap items-center justify-between gap-3",
2164
+ children: [/* @__PURE__ */ jsxs("div", {
2165
+ className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary",
2166
+ children: [
2167
+ /* @__PURE__ */ jsxs(Badge, {
2168
+ variant: "outline",
2169
+ children: [
2170
+ total,
2171
+ " ",
2172
+ tr("fluidgrids.dataTables.rows", "rows")
2173
+ ]
2174
+ }),
2175
+ /* @__PURE__ */ jsxs(Badge, {
2176
+ variant: "outline",
2177
+ children: [
2178
+ table.columns.length,
2179
+ " ",
2180
+ tr("fluidgrids.dataTables.columns", "columns")
2181
+ ]
2182
+ }),
2183
+ /* @__PURE__ */ jsx(Badge, {
2184
+ variant: "outline",
2185
+ children: formatBytes(table.sizeBytes)
2186
+ }),
2187
+ selectedIds.size > 0 && /* @__PURE__ */ jsx(SdkPermissionButton, {
2188
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE,
2189
+ deniedTooltip: tr("fluidgrids.dataTables.deleteRowsDeniedTooltip", "You don't have permission to delete rows"),
2190
+ children: /* @__PURE__ */ jsxs(Button$1, {
2191
+ variant: "outline",
2192
+ size: "sm",
2193
+ className: "whitespace-nowrap",
2194
+ onClick: handleDeleteRows,
2195
+ disabled: deleting,
2196
+ children: [/* @__PURE__ */ jsx(Trash2, {
2197
+ className: "mr-1.5 size-4",
2198
+ "aria-hidden": "true"
2199
+ }), deleting ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteSelected", `Delete ${selectedIds.size} selected`)]
2200
+ })
2201
+ })
2202
+ ]
2203
+ }), /* @__PURE__ */ jsxs("div", {
2204
+ className: "flex flex-wrap items-center gap-2",
2205
+ children: [
2206
+ /* @__PURE__ */ jsx(SdkPermissionButton, {
2207
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
2208
+ deniedTooltip: tr("fluidgrids.dataTables.addRowDeniedTooltip", "You don't have permission to add rows"),
2209
+ children: /* @__PURE__ */ jsxs(Button$1, {
2210
+ size: "sm",
2211
+ className: "whitespace-nowrap",
2212
+ onClick: openNewRow,
2213
+ disabled: importProgress !== null,
2214
+ children: [/* @__PURE__ */ jsx(Plus, {
2215
+ className: "mr-1.5 size-4",
2216
+ "aria-hidden": "true"
2217
+ }), tr("fluidgrids.dataTables.addRow", "Add row")]
2218
+ })
2219
+ }),
2220
+ /* @__PURE__ */ jsx(SdkPermissionButton, {
2221
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
2222
+ deniedTooltip: tr("fluidgrids.dataTables.importDeniedTooltip", "You don't have permission to import rows"),
2223
+ children: /* @__PURE__ */ jsxs(Button$1, {
2224
+ variant: "outline",
2225
+ size: "sm",
2226
+ className: "whitespace-nowrap",
2227
+ onClick: () => fileInputRef.current?.click(),
2228
+ disabled: importProgress !== null,
2229
+ children: [/* @__PURE__ */ jsx(ArrowUpFromLine, {
2230
+ className: "mr-1.5 size-4",
2231
+ "aria-hidden": "true"
2232
+ }), importProgress ? tr("fluidgrids.dataTables.importing", `Importing ${importProgress.done} / ${importProgress.total}…`) : tr("fluidgrids.dataTables.import", "Import CSV")]
2233
+ })
2234
+ }),
2235
+ /* @__PURE__ */ jsx("input", {
2236
+ ref: fileInputRef,
2237
+ type: "file",
2238
+ accept: ".csv,text/csv",
2239
+ className: "hidden",
2240
+ "aria-label": tr("fluidgrids.dataTables.import", "Import CSV"),
2241
+ onChange: handleImport
2242
+ }),
2243
+ /* @__PURE__ */ jsxs(Button$1, {
2244
+ variant: "outline",
2245
+ size: "sm",
2246
+ className: "whitespace-nowrap",
2247
+ onClick: handleExport,
2248
+ disabled: exporting,
2249
+ children: [/* @__PURE__ */ jsx(ArrowDownToLine, {
2250
+ className: "mr-1.5 size-4",
2251
+ "aria-hidden": "true"
2252
+ }), exporting ? tr("fluidgrids.dataTables.exporting", "Exporting…") : tr("fluidgrids.dataTables.export", "Export CSV")]
2253
+ })
2254
+ ]
2255
+ })]
2256
+ })]
2257
+ }),
2258
+ /* @__PURE__ */ jsxs(CardContent, {
2259
+ className: "p-0",
2260
+ children: [
2261
+ error && /* @__PURE__ */ jsx("div", {
2262
+ className: "m-4 rounded-md border border-border-secondary bg-background-secondary p-3 text-sm text-text-secondary",
2263
+ children: v2 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: tr("fluidgrids.dataTables.rowsLoadFailed", "We couldn't load the rows. Try again in a moment.") }), /* @__PURE__ */ jsx(TechnicalDetails, {
2264
+ className: "mt-2",
2265
+ children: /* @__PURE__ */ jsx("code", {
2266
+ className: "break-words text-xs",
2267
+ children: error.message
2268
+ })
2269
+ })] }) : error.message
2270
+ }),
2271
+ v2 && (rows.length > 0 || rowSearch !== "") && /* @__PURE__ */ jsxs("div", {
2272
+ className: "relative mx-4 mt-4 max-w-sm",
2273
+ children: [/* @__PURE__ */ jsx(Search, {
2274
+ className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-text-secondary",
2275
+ "aria-hidden": "true"
2276
+ }), /* @__PURE__ */ jsx(Input, {
2277
+ className: "pl-8",
2278
+ value: rowSearch,
2279
+ onChange: (event) => setRowSearch(event.target.value),
2280
+ placeholder: tr("fluidgrids.dataTables.searchRows", "Search this page of rows"),
2281
+ "aria-label": tr("fluidgrids.dataTables.searchRows", "Search this page of rows"),
2282
+ "data-testid": "row-search"
2283
+ })]
2284
+ }),
2285
+ /* @__PURE__ */ jsx("div", {
2286
+ className: "overflow-x-auto",
2287
+ children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
2288
+ (!v2 || selectMode) && /* @__PURE__ */ jsx(TableHead, {
2289
+ className: "w-10",
2290
+ children: /* @__PURE__ */ jsx("input", {
2291
+ type: "checkbox",
2292
+ checked: allVisibleSelected,
2293
+ onChange: toggleAll,
2294
+ "aria-label": tr("fluidgrids.dataTables.selectAll", "Select all rows on this page")
2295
+ })
2296
+ }),
2297
+ (!v2 || showRowIds) && /* @__PURE__ */ jsx(TableHead, { children: tr("fluidgrids.dataTables.rowId", "Row") }),
2298
+ table.columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
2299
+ "aria-sort": v2 && sort?.key === column.key ? sort.direction === 1 ? "ascending" : "descending" : void 0,
2300
+ children: v2 ? /* @__PURE__ */ jsxs("button", {
2301
+ type: "button",
2302
+ className: "inline-flex items-center gap-1 font-medium hover:text-text-primary",
2303
+ onClick: () => toggleSort(column.key),
2304
+ "aria-label": tr("fluidgrids.dataTables.sortBy", "Sort by {{column}}", { column: column.label }),
2305
+ children: [column.label, sort?.key === column.key && /* @__PURE__ */ jsx("span", {
2306
+ "aria-hidden": "true",
2307
+ children: sort.direction === 1 ? "▲" : "▼"
2308
+ })]
2309
+ }) : column.label
2310
+ }, column.key)),
2311
+ /* @__PURE__ */ jsx(TableHead, {
2312
+ className: "text-right",
2313
+ children: tr("fluidgrids.dataTables.actions", "Actions")
2314
+ })
2315
+ ] }) }), /* @__PURE__ */ jsxs(TableBody, { children: [
2316
+ loading && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
2317
+ colSpan: bodyColumnCount,
2318
+ className: "py-10 text-center text-text-secondary",
2319
+ children: tr("fluidgrids.dataTables.loadingRows", "Loading rows…")
2320
+ }) }),
2321
+ !loading && rows.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
2322
+ colSpan: bodyColumnCount,
2323
+ className: "py-10 text-center text-text-secondary",
2324
+ children: tr("fluidgrids.dataTables.emptyRows", v2 ? "No rows yet. Add a row or import a spreadsheet (CSV) to get started." : "No rows yet. Add a row or import a CSV to get started.")
2325
+ }) }),
2326
+ v2 && !loading && rows.length > 0 && displayRows.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
2327
+ colSpan: bodyColumnCount,
2328
+ className: "py-10 text-center text-text-secondary",
2329
+ children: tr("fluidgrids.dataTables.noRowsMatch", "No rows on this page match your search.")
2330
+ }) }),
2331
+ !loading && displayRows.map((row) => /* @__PURE__ */ jsxs(TableRow, { children: [
2332
+ (!v2 || selectMode) && /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("input", {
2333
+ type: "checkbox",
2334
+ checked: selectedIds.has(row.id),
2335
+ onChange: () => setSelectedIds((current) => {
2336
+ const next = new Set(current);
2337
+ if (next.has(row.id)) next.delete(row.id);
2338
+ else next.add(row.id);
2339
+ return next;
2340
+ }),
2341
+ "aria-label": tr("fluidgrids.dataTables.selectRow", "Select row")
2342
+ }) }),
2343
+ (!v2 || showRowIds) && /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("code", {
2344
+ className: "text-xs text-text-secondary",
2345
+ children: row.id.slice(0, 8)
2346
+ }) }),
2347
+ table.columns.map((column) => {
2348
+ const text = v2 ? formatCell(row.data[column.key], column.type, yesNo) : formatValue(row.data[column.key]);
2349
+ return /* @__PURE__ */ jsx(TableCell, {
2350
+ className: "max-w-[16rem] truncate text-sm",
2351
+ title: text,
2352
+ children: text
2353
+ }, column.key);
2354
+ }),
2355
+ /* @__PURE__ */ jsx(TableCell, {
2356
+ className: "text-right",
2357
+ children: v2 ? /* @__PURE__ */ jsx(RunActionsMenu, {
2358
+ label: tr("fluidgrids.dataTables.rowActions", "Row actions"),
2359
+ "data-testid": "row-actions-menu",
2360
+ items: [...canEditRows ? [{
2361
+ id: "edit",
2362
+ label: tr("fluidgrids.dataTables.editRow", "Edit row"),
2363
+ icon: "mdi:pencil-outline",
2364
+ onSelect: () => openEditRow(row)
2365
+ }] : [], ...canDeleteRows ? [{
2366
+ id: "delete",
2367
+ label: tr("fluidgrids.dataTables.deleteRow", "Delete row"),
2368
+ icon: "mdi:trash-can-outline",
2369
+ destructive: true,
2370
+ onSelect: () => setPendingRowDelete([row.id])
2371
+ }] : []]
2372
+ }) : /* @__PURE__ */ jsx(SdkPermissionButton, {
2373
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_UPDATE,
2374
+ deniedTooltip: tr("fluidgrids.dataTables.editRowDeniedTooltip", "You don't have permission to edit rows"),
2375
+ children: /* @__PURE__ */ jsx(Button$1, {
2376
+ variant: "ghost",
2377
+ size: "icon",
2378
+ onClick: () => openEditRow(row),
2379
+ title: tr("fluidgrids.dataTables.editRow", "Edit row"),
2380
+ "aria-label": tr("fluidgrids.dataTables.editRow", "Edit row"),
2381
+ children: /* @__PURE__ */ jsx(Pencil, {
2382
+ className: "size-4",
2383
+ "aria-hidden": "true"
2384
+ })
2385
+ })
2386
+ })
2387
+ })
2388
+ ] }, row.id))
2389
+ ] })] })
2390
+ }),
2391
+ total > 0 && /* @__PURE__ */ jsxs("div", {
2392
+ className: "flex flex-wrap items-center justify-between gap-3 border-t border-border-secondary px-4 py-3 text-xs text-text-secondary",
2393
+ children: [/* @__PURE__ */ jsx("span", {
2394
+ "aria-live": "polite",
2395
+ children: tr("fluidgrids.dataTables.showingRange", `Showing ${range.from}–${range.to} of ${total}`)
2396
+ }), /* @__PURE__ */ jsxs("div", {
2397
+ className: "flex flex-wrap items-center gap-3",
2398
+ children: [
2399
+ !v2 && /* @__PURE__ */ jsxs("label", {
2400
+ className: "flex items-center gap-2",
2401
+ children: [/* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.rowsPerPage", "Rows per page") }), /* @__PURE__ */ jsx("select", {
2402
+ className: "h-8 rounded-md border border-input bg-background px-2 text-xs text-text-primary",
2403
+ value: pageSize,
2404
+ onChange: (event) => {
2405
+ setPageSize(Number(event.target.value));
2406
+ setPage(0);
2407
+ },
2408
+ children: ROW_PAGE_SIZES.map((size) => /* @__PURE__ */ jsx("option", {
2409
+ value: size,
2410
+ children: size
2411
+ }, size))
2412
+ })]
2413
+ }),
2414
+ /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.pageOf", `Page ${page + 1} of ${pages}`) }),
2415
+ /* @__PURE__ */ jsxs("div", {
2416
+ className: "flex items-center gap-1",
2417
+ children: [/* @__PURE__ */ jsx(Button$1, {
2418
+ variant: "outline",
2419
+ size: "icon",
2420
+ className: "size-8",
2421
+ onClick: () => setPage((current) => Math.max(current - 1, 0)),
2422
+ disabled: page === 0 || loading,
2423
+ title: tr("fluidgrids.dataTables.prevPage", "Previous page"),
2424
+ "aria-label": tr("fluidgrids.dataTables.prevPage", "Previous page"),
2425
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
2426
+ className: "size-4",
2427
+ "aria-hidden": "true"
2428
+ })
2429
+ }), /* @__PURE__ */ jsx(Button$1, {
2430
+ variant: "outline",
2431
+ size: "icon",
2432
+ className: "size-8",
2433
+ onClick: () => setPage((current) => Math.min(current + 1, pages - 1)),
2434
+ disabled: page >= pages - 1 || loading,
2435
+ title: tr("fluidgrids.dataTables.nextPage", "Next page"),
2436
+ "aria-label": tr("fluidgrids.dataTables.nextPage", "Next page"),
2437
+ children: /* @__PURE__ */ jsx(ChevronRight, {
2438
+ className: "size-4",
2439
+ "aria-hidden": "true"
2440
+ })
2441
+ })]
2442
+ })
2443
+ ]
2444
+ })]
2445
+ })
2446
+ ]
2447
+ }),
2448
+ v2 ? /* @__PURE__ */ jsx(RowFormDialogV2, {
2449
+ open: rowDialogOpen,
2450
+ table,
2451
+ row: editingRow,
2452
+ onOpenChange: setRowDialogOpen,
2453
+ onSaved: async () => {
2454
+ await refetch();
2455
+ onTableChanged();
2456
+ }
2457
+ }) : /* @__PURE__ */ jsx(RowFormDialog, {
2458
+ open: rowDialogOpen,
2459
+ table,
2460
+ row: editingRow,
2461
+ onOpenChange: setRowDialogOpen,
2462
+ onSaved: async () => {
2463
+ await refetch();
2464
+ onTableChanged();
2465
+ }
2466
+ }),
2467
+ v2 && /* @__PURE__ */ jsx(ConfirmActionDialog, {
2468
+ open: pendingRowDelete !== null,
2469
+ onOpenChange: (open) => {
2470
+ if (!open) setPendingRowDelete(null);
2471
+ },
2472
+ title: (pendingRowDelete?.length ?? 0) === 1 ? tr("fluidgrids.dataTables.deleteRowTitle", "Delete this row?") : tr("fluidgrids.dataTables.deleteRowsTitle", "Delete {{count}} rows?", { count: pendingRowDelete?.length ?? 0 }),
2473
+ description: tr("fluidgrids.dataTables.deleteRowsBody", "This permanently removes it from \"{{table}}\". Workflows will no longer find it.", { table: table.name }),
2474
+ confirmLabel: tr("fluidgrids.dataTables.delete", "Delete"),
2475
+ cancelLabel: tr("fluidgrids.dataTables.keep", "Keep"),
2476
+ destructive: true,
2477
+ busy: deleting,
2478
+ onConfirm: () => void confirmRowDelete(),
2479
+ "data-testid": "confirm-delete-rows"
2480
+ })
2481
+ ]
2482
+ });
2483
+ }
2484
+ function DataTablesPage() {
2485
+ const tr = useTr();
2486
+ const v2 = useNavigationV2();
2487
+ const navigate = useNavigate();
2488
+ const createTablePermission = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE);
2489
+ const canCreateTable = createTablePermission.allowed;
2490
+ const [searchInput, setSearchInput] = useState("");
2491
+ const [search, setSearch] = useState("");
2492
+ const [tagFilter, setTagFilter] = useState("");
2493
+ const { tables, total, loading, error, refetch } = useDataTables(search, tagFilter);
2494
+ const { tagsById, refetch: refetchTags } = useDataTableTagOverlay();
2495
+ const { deleteDataTable, loading: deleting } = useDeleteDataTable();
2496
+ const [selectedId, setSelectedId] = useState(null);
2497
+ const [tableDialogOpen, setTableDialogOpen] = useState(false);
2498
+ const [editingTable, setEditingTable] = useState(null);
2499
+ const [confirmingDeleteTable, setConfirmingDeleteTable] = useState(false);
2500
+ const newTableRequest = useActionParam(ACTION_PARAM_VALUES.new, createTablePermission);
2501
+ const openCreateTable = useCallback(() => {
2502
+ setEditingTable(null);
2503
+ setTableDialogOpen(true);
2504
+ }, []);
2505
+ useOpenRequest(newTableRequest.requested, openCreateTable, newTableRequest.handled);
2506
+ const selectedTable = useMemo(() => tables.find((table) => table.id === selectedId) ?? null, [selectedId, tables]);
2507
+ useEffect(() => {
2508
+ if (tables.length === 0) {
2509
+ setSelectedId(null);
2510
+ return;
2511
+ }
2512
+ if (!selectedId || !tables.some((table) => table.id === selectedId)) setSelectedId(tables[0].id);
2513
+ }, [selectedId, tables]);
2514
+ const refresh = useCallback(async () => {
2515
+ await refetch();
2516
+ }, [refetch]);
2517
+ const handleTagsChanged = useCallback(() => {
2518
+ refetchTags();
2519
+ if (tagFilter) refetch();
2520
+ }, [
2521
+ refetch,
2522
+ refetchTags,
2523
+ tagFilter
2524
+ ]);
2525
+ const performDeleteTable = useCallback(async () => {
2526
+ if (!selectedTable) return;
2527
+ try {
2528
+ await deleteDataTable(selectedTable.id);
2529
+ toast.success(tr("fluidgrids.dataTables.deleted", "Data table deleted"));
2530
+ setSelectedId(null);
2531
+ await refetch();
2532
+ } catch (error) {
2533
+ toast.error(describeOperationError(error, tr("fluidgrids.dataTables.deleteFailed", "Failed to delete data table")));
2534
+ }
2535
+ }, [
2536
+ deleteDataTable,
2537
+ refetch,
2538
+ selectedTable,
2539
+ tr
2540
+ ]);
2541
+ const handleDeleteTable = useCallback(async () => {
2542
+ if (!selectedTable) return;
2543
+ if (v2) {
2544
+ setConfirmingDeleteTable(true);
2545
+ return;
2546
+ }
2547
+ if (!window.confirm(tr("fluidgrids.dataTables.confirmDeleteTable", `Delete table "${selectedTable.name}" and all of its rows?`))) return;
2548
+ await performDeleteTable();
2549
+ }, [
2550
+ performDeleteTable,
2551
+ selectedTable,
2552
+ tr,
2553
+ v2
2554
+ ]);
2555
+ const confirmDeleteTable = useCallback(async () => {
2556
+ await performDeleteTable();
2557
+ setConfirmingDeleteTable(false);
2558
+ }, [performDeleteTable]);
2559
+ const handleTableSaved = useCallback((table) => {
2560
+ if (table) setSelectedId(table.id);
2561
+ refetch();
2562
+ }, [refetch]);
2563
+ const submitSearch = useCallback((event) => {
2564
+ event.preventDefault();
2565
+ setSearch(searchInput);
2566
+ }, [searchInput]);
2567
+ return /* @__PURE__ */ jsxs("div", {
2568
+ className: "min-h-screen bg-background-primary p-4 pb-24 sm:p-6 sm:pb-24",
2569
+ children: [
2570
+ /* @__PURE__ */ jsxs("div", {
2571
+ className: "mx-auto flex max-w-[1500px] flex-col gap-5",
2572
+ children: [
2573
+ /* @__PURE__ */ jsxs("div", {
2574
+ className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
2575
+ "data-tour": "fluidgrids-data-tables-header",
2576
+ children: [/* @__PURE__ */ jsxs("div", { children: [
2577
+ /* @__PURE__ */ jsxs("div", {
2578
+ className: "mb-2 flex items-center gap-2 text-sm text-text-secondary",
2579
+ children: [/* @__PURE__ */ jsx(Database, {
2580
+ className: "size-4",
2581
+ "aria-hidden": "true"
2582
+ }), tr("fluidgrids.dataTables.workflowData", "Workflow data")]
2583
+ }),
2584
+ /* @__PURE__ */ jsx("h1", {
2585
+ className: "text-2xl font-bold text-text-primary sm:text-3xl",
2586
+ children: tr("fluidgrids.dataTables.pageTitle", v2 ? "Data tables" : "Data Tables")
2587
+ }),
2588
+ /* @__PURE__ */ jsx(PagePurpose, {
2589
+ icon: /* @__PURE__ */ jsx(Database, {
2590
+ className: "size-4",
2591
+ "aria-hidden": "true"
2592
+ }),
2593
+ children: tr("fluidgrids.dataTables.pageSubtitle", v2 ? "Keep a simple list your workflows can read and add to, like a small spreadsheet every run can use." : "Use Data Tables as lightweight persistent storage for workflow state, lookups, and records shared across runs.")
2594
+ }),
2595
+ /* @__PURE__ */ jsx(FluidGridsDocsLink, {
2596
+ topic: "dataTables",
2597
+ className: "mt-1"
2598
+ })
2599
+ ] }), /* @__PURE__ */ jsxs("div", {
2600
+ className: "flex flex-wrap gap-2",
2601
+ children: [/* @__PURE__ */ jsxs(Button$1, {
2602
+ variant: "outline",
2603
+ onClick: () => navigate("../list"),
2604
+ children: [/* @__PURE__ */ jsx(ArrowLeft, {
2605
+ className: "mr-2 size-4",
2606
+ "aria-hidden": "true"
2607
+ }), tr("fluidgrids.dataTables.backToWorkflows", "Back to Workflows")]
2608
+ }), /* @__PURE__ */ jsx(SdkPermissionButton, {
2609
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE,
2610
+ deniedTooltip: tr("fluidgrids.dataTables.createDeniedTooltip", "You don't have permission to create data tables"),
2611
+ children: /* @__PURE__ */ jsxs(Button$1, {
2612
+ onClick: () => {
2613
+ setEditingTable(null);
2614
+ setTableDialogOpen(true);
2615
+ },
2616
+ "data-tour": "fluidgrids-data-tables-create",
2617
+ children: [/* @__PURE__ */ jsx(Plus, {
2618
+ className: "mr-2 size-4",
2619
+ "aria-hidden": "true"
2620
+ }), tr("fluidgrids.dataTables.newTable", v2 ? "New table" : "New data table")]
2621
+ })
2622
+ })]
2623
+ })]
2624
+ }),
2625
+ /* @__PURE__ */ jsx(NextSteps, {
2626
+ storageKey: "data-tables",
2627
+ title: tr("fluidgrids.dataTables.nextStepsTitle", v2 ? "How to use data tables" : "How to use Data Tables"),
2628
+ steps: [
2629
+ {
2630
+ id: "create",
2631
+ label: tr("fluidgrids.dataTables.nextStepCreateLabel", "Create a table"),
2632
+ description: tr("fluidgrids.dataTables.nextStepCreateDescription", v2 ? "Name the columns for the information your workflow needs." : "Define typed columns for the records your workflow needs."),
2633
+ icon: /* @__PURE__ */ jsx(Plus, {
2634
+ className: "size-4",
2635
+ "aria-hidden": "true"
2636
+ }),
2637
+ onClick: canCreateTable ? () => {
2638
+ setEditingTable(null);
2639
+ setTableDialogOpen(true);
2640
+ } : void 0
2641
+ },
2642
+ {
2643
+ id: "connect",
2644
+ label: tr("fluidgrids.dataTables.nextStepConnectLabel", "Connect it to a workflow"),
2645
+ description: tr("fluidgrids.dataTables.nextStepConnectDescription", v2 ? "Add a Table step to a workflow to read or save rows." : "Add a Data Table node to get, insert, update, upsert, or delete rows."),
2646
+ icon: /* @__PURE__ */ jsx(Table2, {
2647
+ className: "size-4",
2648
+ "aria-hidden": "true"
2649
+ }),
2650
+ onClick: () => navigate("../list")
2651
+ },
2652
+ {
2653
+ id: "reuse",
2654
+ label: tr("fluidgrids.dataTables.nextStepReuseLabel", "Reuse data across runs"),
2655
+ description: tr("fluidgrids.dataTables.nextStepReuseDescription", v2 ? "Remember what earlier runs did, for example which emails were already sent." : "Use saved status, cursors, lookups, and deduplication keys on later runs."),
2656
+ icon: /* @__PURE__ */ jsx(RefreshCw, {
2657
+ className: "size-4",
2658
+ "aria-hidden": "true"
2659
+ })
2660
+ }
2661
+ ]
2662
+ }),
2663
+ /* @__PURE__ */ jsxs("div", {
2664
+ className: "grid gap-5 xl:grid-cols-[minmax(16rem,22rem)_minmax(0,1fr)]",
2665
+ children: [
2666
+ /* @__PURE__ */ jsxs(Card, {
2667
+ className: "h-fit border-border-secondary",
2668
+ "data-tour": "fluidgrids-data-tables-list",
2669
+ children: [/* @__PURE__ */ jsxs(CardHeader, {
2670
+ className: "gap-3 border-b border-border-secondary p-4",
2671
+ children: [
2672
+ /* @__PURE__ */ jsxs("div", {
2673
+ className: "flex items-center justify-between gap-2",
2674
+ children: [/* @__PURE__ */ jsx(CardTitle, {
2675
+ className: "text-base",
2676
+ children: tr("fluidgrids.dataTables.tables", "Tables")
2677
+ }), /* @__PURE__ */ jsx(Badge, {
2678
+ variant: "outline",
2679
+ children: total
2680
+ })]
2681
+ }),
2682
+ /* @__PURE__ */ jsxs("form", {
2683
+ className: "flex gap-2",
2684
+ onSubmit: submitSearch,
2685
+ children: [/* @__PURE__ */ jsxs("div", {
2686
+ className: "relative min-w-0 flex-1",
2687
+ children: [/* @__PURE__ */ jsx(Search, {
2688
+ className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-text-secondary",
2689
+ "aria-hidden": "true"
2690
+ }), /* @__PURE__ */ jsx(Input, {
2691
+ className: "pl-8",
2692
+ value: searchInput,
2693
+ onChange: (event) => setSearchInput(event.target.value),
2694
+ placeholder: tr("fluidgrids.dataTables.search", "Search tables"),
2695
+ "aria-label": tr("fluidgrids.dataTables.search", "Search tables")
2696
+ })]
2697
+ }), /* @__PURE__ */ jsx(Button$1, {
2698
+ type: "submit",
2699
+ variant: "outline",
2700
+ size: "sm",
2701
+ children: tr("fluidgrids.dataTables.searchAction", "Search")
2702
+ })]
2703
+ }),
2704
+ v2 ? /* @__PURE__ */ jsx(Disclosure, {
2705
+ closedLabel: tr("fluidgrids.dataTables.moreFilters", "More filters"),
2706
+ openLabel: tr("fluidgrids.dataTables.fewerFilters", "Fewer filters"),
2707
+ defaultOpen: tagFilter !== "",
2708
+ "data-testid": "table-more-filters",
2709
+ children: /* @__PURE__ */ jsx(TagFilterSelect, {
2710
+ value: tagFilter,
2711
+ onChange: setTagFilter,
2712
+ className: "w-full"
2713
+ })
2714
+ }) : /* @__PURE__ */ jsx(TagFilterSelect, {
2715
+ value: tagFilter,
2716
+ onChange: setTagFilter,
2717
+ className: "w-full"
2718
+ })
2719
+ ]
2720
+ }), /* @__PURE__ */ jsxs(CardContent, {
2721
+ className: "max-h-72 overflow-y-auto p-2 xl:max-h-[36rem]",
2722
+ children: [
2723
+ loading && /* @__PURE__ */ jsx("p", {
2724
+ className: "p-4 text-sm text-text-secondary",
2725
+ children: tr("fluidgrids.dataTables.loading", "Loading tables…")
2726
+ }),
2727
+ !loading && tables.length === 0 && tagFilter && /* @__PURE__ */ jsx("p", {
2728
+ className: "p-4 text-sm text-text-secondary",
2729
+ children: tr("fluidgrids.dataTables.noTablesWithTag", "No tables carry this tag.")
2730
+ }),
2731
+ !loading && tables.length === 0 && !tagFilter && /* @__PURE__ */ jsx(IllustratedEmptyState, {
2732
+ className: "py-8",
2733
+ illustration: "onboarding",
2734
+ title: tr("fluidgrids.dataTables.empty", v2 ? "Create your first table" : "Create your first Data Table"),
2735
+ description: tr("fluidgrids.dataTables.emptyHint", v2 ? "Give your workflows a shared place to keep records between runs." : "Give your workflows a shared place to store typed records across runs."),
2736
+ action: /* @__PURE__ */ jsx(SdkPermissionButton, {
2737
+ permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE,
2738
+ deniedTooltip: tr("fluidgrids.dataTables.createDeniedTooltip", "You don't have permission to create data tables"),
2739
+ children: /* @__PURE__ */ jsxs(Button$1, {
2740
+ size: "sm",
2741
+ onClick: () => {
2742
+ setEditingTable(null);
2743
+ setTableDialogOpen(true);
2744
+ },
2745
+ children: [/* @__PURE__ */ jsx(Plus, {
2746
+ className: "mr-2 size-4",
2747
+ "aria-hidden": "true"
2748
+ }), tr("fluidgrids.dataTables.createFirstTable", "Create a table")]
2749
+ })
2750
+ })
2751
+ }),
2752
+ !loading && tables.map((table) => /* @__PURE__ */ jsxs("button", {
2753
+ type: "button",
2754
+ onClick: () => setSelectedId(table.id),
2755
+ 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"}`,
2756
+ children: [/* @__PURE__ */ jsxs("span", {
2757
+ className: "min-w-0",
2758
+ children: [
2759
+ /* @__PURE__ */ jsx("span", {
2760
+ className: "block truncate text-sm font-medium",
2761
+ children: table.name
2762
+ }),
2763
+ /* @__PURE__ */ jsxs("span", {
2764
+ className: "mt-1 block text-xs text-text-secondary",
2765
+ children: [
2766
+ table.rowCount,
2767
+ " ",
2768
+ tr("fluidgrids.dataTables.rows", "rows"),
2769
+ " ·",
2770
+ " ",
2771
+ table.columns.length,
2772
+ " ",
2773
+ tr("fluidgrids.dataTables.columns", "columns")
2774
+ ]
2775
+ }),
2776
+ (!v2 || tagFilter !== "") && (tagsById[table.id] ?? []).length > 0 && /* @__PURE__ */ jsx("span", {
2777
+ className: "mt-1.5 flex flex-wrap gap-1",
2778
+ children: (tagsById[table.id] ?? []).map((assignment) => /* @__PURE__ */ jsx(TagBadge, {
2779
+ id: assignment.tagId,
2780
+ label: tagLabel(assignment),
2781
+ color: assignment.tagInfo?.color ?? void 0,
2782
+ size: "sm"
2783
+ }, assignment.tagId))
2784
+ })
2785
+ ]
2786
+ }), /* @__PURE__ */ jsx(Table2, {
2787
+ className: "mt-0.5 size-4 shrink-0",
2788
+ "aria-hidden": "true"
2789
+ })]
2790
+ }, table.id))
2791
+ ]
2792
+ })]
2793
+ }),
2794
+ error && !selectedTable && /* @__PURE__ */ jsx(Card, {
2795
+ className: "border-border-secondary",
2796
+ children: /* @__PURE__ */ jsxs(CardContent, {
2797
+ className: "p-6 text-sm text-text-secondary",
2798
+ children: [v2 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: tr("fluidgrids.dataTables.loadFailed", "We couldn't load your tables. Try again in a moment.") }), /* @__PURE__ */ jsx(TechnicalDetails, {
2799
+ className: "mt-2",
2800
+ children: /* @__PURE__ */ jsx("code", {
2801
+ className: "break-words text-xs",
2802
+ children: error.message
2803
+ })
2804
+ })] }) : /* @__PURE__ */ jsx("p", { children: error.message }), /* @__PURE__ */ jsxs(Button$1, {
2805
+ className: "mt-4",
2806
+ variant: "outline",
2807
+ onClick: () => void refresh(),
2808
+ children: [/* @__PURE__ */ jsx(RefreshCw, {
2809
+ className: "mr-2 size-4",
2810
+ "aria-hidden": "true"
2811
+ }), tr("fluidgrids.dataTables.retry", "Try again")]
2812
+ })]
2813
+ })
2814
+ }),
2815
+ !error && !selectedTable && !loading && !(v2 && tables.length === 0 && tagFilter === "") && /* @__PURE__ */ jsx(Card, {
2816
+ className: "border-border-secondary",
2817
+ children: /* @__PURE__ */ jsxs(CardContent, {
2818
+ className: "flex min-h-[22rem] flex-col items-center justify-center p-6 text-center",
2819
+ children: [
2820
+ /* @__PURE__ */ jsx(Database, {
2821
+ className: "mb-3 size-10 text-text-secondary",
2822
+ "aria-hidden": "true"
2823
+ }),
2824
+ /* @__PURE__ */ jsx("h2", {
2825
+ className: "text-lg font-semibold text-text-primary",
2826
+ children: tr("fluidgrids.dataTables.selectTitle", v2 ? "Select a table" : "Select a data table")
2827
+ }),
2828
+ /* @__PURE__ */ jsx("p", {
2829
+ className: "mt-1 max-w-sm text-sm text-text-secondary",
2830
+ children: tr("fluidgrids.dataTables.selectDescription", "Choose a table to view and edit its rows, or create your first table.")
2831
+ })
2832
+ ]
2833
+ })
2834
+ }),
2835
+ selectedTable && /* @__PURE__ */ jsx("div", {
2836
+ className: "min-w-0",
2837
+ children: /* @__PURE__ */ jsx(TableRowsPanel, {
2838
+ table: selectedTable,
2839
+ onTableChanged: refresh,
2840
+ onEditTable: () => {
2841
+ setEditingTable(selectedTable);
2842
+ setTableDialogOpen(true);
2843
+ },
2844
+ onDeleteTable: handleDeleteTable,
2845
+ deletingTable: deleting,
2846
+ onTagsChanged: handleTagsChanged
2847
+ }, selectedTable.id)
2848
+ })
2849
+ ]
2850
+ })
2851
+ ]
2852
+ }),
2853
+ v2 ? /* @__PURE__ */ jsx(TableFormDialogV2, {
2854
+ open: tableDialogOpen,
2855
+ table: editingTable,
2856
+ onOpenChange: setTableDialogOpen,
2857
+ onSaved: handleTableSaved
2858
+ }) : /* @__PURE__ */ jsx(TableFormDialog, {
2859
+ open: tableDialogOpen,
2860
+ table: editingTable,
2861
+ onOpenChange: setTableDialogOpen,
2862
+ onSaved: handleTableSaved
2863
+ }),
2864
+ v2 && /* @__PURE__ */ jsx(ConfirmActionDialog, {
2865
+ open: confirmingDeleteTable,
2866
+ onOpenChange: setConfirmingDeleteTable,
2867
+ title: tr("fluidgrids.dataTables.deleteTableTitle", "Delete \"{{name}}\"?", { name: selectedTable?.name ?? "" }),
2868
+ description: tr("fluidgrids.dataTables.deleteTableBody", "This permanently deletes the table and the {{count}} row(s) in it. Workflows that use it will stop working until you point them at another table.", { count: selectedTable?.rowCount ?? 0 }),
2869
+ confirmLabel: tr("fluidgrids.dataTables.delete", "Delete"),
2870
+ cancelLabel: tr("fluidgrids.dataTables.keep", "Keep"),
2871
+ destructive: true,
2872
+ busy: deleting,
2873
+ onConfirm: () => void confirmDeleteTable(),
2874
+ "data-testid": "confirm-delete-table"
2875
+ })
2876
+ ]
2877
+ });
2878
+ }
2879
+
2880
+ 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 };