@burdenoff/fluidgrids-fe-sdk 2026.1007.1 → 2026.1007.3

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.
package/dist/index.d.ts CHANGED
@@ -777,7 +777,12 @@ declare function useDataTables(search?: string): {
777
777
  error: Error | null;
778
778
  refetch: () => Promise<void>;
779
779
  };
780
- declare function useDataTableRows(tableId: string | null): {
780
+ interface DataTableRowsPaging {
781
+ /** Zero-based page index. */
782
+ page: number;
783
+ pageSize: number;
784
+ }
785
+ declare function useDataTableRows(tableId: string | null, paging: DataTableRowsPaging): {
781
786
  rows: DataTableRow$1[];
782
787
  total: number;
783
788
  loading: boolean;
package/dist/index.js CHANGED
@@ -46,10 +46,10 @@ import { Icon } from '@iconify/react';
46
46
  import { ErrorBoundary } from '@burdenoff/fe-libs/shared/components';
47
47
  import '@xyflow/react/dist/style.css';
48
48
  import { Button } from '@burdenoff/fe-libs/button';
49
- import { Tabs, TabsList, TabsTrigger, TabsContent, Card, CardContent, CardHeader, CardTitle, Badge, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, Label, Switch, CardDescription, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Textarea } from '@burdenoff/fe-libs/ui';
49
+ import { Tabs, TabsList, TabsTrigger, TabsContent, Card, CardContent, PagePurpose, NextSteps, CardHeader, CardTitle, Badge, Input, IllustratedEmptyState, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, Label, Switch, CardDescription, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Textarea } from '@burdenoff/fe-libs/ui';
50
50
  import { toast } from 'sonner';
51
51
  import { gql } from '@apollo/client';
52
- import { ChevronRight, Check, Circle, Database, ArrowLeft, Plus, Search, Table2, RefreshCw, Pencil, Trash2, ChevronDown, ChevronUp, ArrowUpFromLine, ArrowDownToLine } from 'lucide-react';
52
+ import { ChevronRight, Check, Circle, Database, ArrowLeft, Plus, Table2, RefreshCw, Search, ChevronDown, ChevronUp, Pencil, Trash2, ArrowUpFromLine, ArrowDownToLine, ChevronLeft } from 'lucide-react';
53
53
  import * as TabsPrimitive from '@radix-ui/react-tabs';
54
54
  import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
55
55
  import * as SeparatorPrimitive from '@radix-ui/react-separator';
@@ -1745,7 +1745,38 @@ function useDataTables(search) {
1745
1745
  }, [fetchTables]);
1746
1746
  return { tables, total, loading, error, refetch: fetchTables };
1747
1747
  }
1748
- function useDataTableRows(tableId) {
1748
+ var DATA_TABLE_MAX_PAGE_SIZE = 5e3;
1749
+ async function queryRowsPage(tableId, limit, offset) {
1750
+ const result = throwIfResultError(
1751
+ await getFluidGridsClient().query({
1752
+ query: GET_DATA_TABLE_ROWS,
1753
+ variables: { input: { tableId, limit, offset } },
1754
+ fetchPolicy: "network-only"
1755
+ })
1756
+ );
1757
+ const response = result.data?.dataTableRows;
1758
+ return {
1759
+ rows: response?.rows.map(toRow) ?? [],
1760
+ total: response?.total ?? 0
1761
+ };
1762
+ }
1763
+ async function fetchAllDataTableRows(tableId) {
1764
+ const collected = [];
1765
+ let total = Number.POSITIVE_INFINITY;
1766
+ while (collected.length < total) {
1767
+ const page = await queryRowsPage(
1768
+ tableId,
1769
+ DATA_TABLE_MAX_PAGE_SIZE,
1770
+ collected.length
1771
+ );
1772
+ total = page.total;
1773
+ if (page.rows.length === 0) break;
1774
+ collected.push(...page.rows);
1775
+ }
1776
+ return collected;
1777
+ }
1778
+ function useDataTableRows(tableId, paging) {
1779
+ const { page, pageSize } = paging;
1749
1780
  const [rows, setRows] = useState([]);
1750
1781
  const [total, setTotal] = useState(0);
1751
1782
  const [loading, setLoading] = useState(false);
@@ -1761,22 +1792,15 @@ function useDataTableRows(tableId) {
1761
1792
  setLoading(true);
1762
1793
  setError(null);
1763
1794
  try {
1764
- const result = throwIfResultError(
1765
- await getFluidGridsClient().query({
1766
- query: GET_DATA_TABLE_ROWS,
1767
- variables: { input: { tableId, limit: 5e3, offset: 0 } },
1768
- fetchPolicy: "network-only"
1769
- })
1770
- );
1771
- const response = result.data?.dataTableRows;
1772
- setRows(response?.rows.map(toRow) ?? []);
1773
- setTotal(response?.total ?? 0);
1795
+ const response = await queryRowsPage(tableId, pageSize, page * pageSize);
1796
+ setRows(response.rows);
1797
+ setTotal(response.total);
1774
1798
  } catch (err) {
1775
1799
  setError(operationError(err, "Failed to load data table rows"));
1776
1800
  } finally {
1777
1801
  setLoading(false);
1778
1802
  }
1779
- }, [tableId]);
1803
+ }, [tableId, page, pageSize]);
1780
1804
  useEffect(() => {
1781
1805
  void fetchRows();
1782
1806
  }, [fetchRows]);
@@ -2030,6 +2054,22 @@ function rowsToCsv(columns, rows) {
2030
2054
  );
2031
2055
  return [header, ...values].join("\r\n");
2032
2056
  }
2057
+
2058
+ // src/modules/data-tables/utils/paging.ts
2059
+ var ROW_PAGE_SIZES = [50, 100, 250];
2060
+ var DEFAULT_ROW_PAGE_SIZE = ROW_PAGE_SIZES[0];
2061
+ function pageCount(total, pageSize) {
2062
+ if (pageSize <= 0) return 1;
2063
+ return Math.max(1, Math.ceil(total / pageSize));
2064
+ }
2065
+ function clampPage(page, total, pageSize) {
2066
+ return Math.min(Math.max(page, 0), pageCount(total, pageSize) - 1);
2067
+ }
2068
+ function pageRange(page, pageSize, shown) {
2069
+ if (shown <= 0) return { from: 0, to: 0 };
2070
+ const from = page * pageSize + 1;
2071
+ return { from, to: from + shown - 1 };
2072
+ }
2033
2073
  var COLUMN_TYPES = [
2034
2074
  "STRING",
2035
2075
  "NUMBER",
@@ -2440,15 +2480,35 @@ function RowFormDialog({
2440
2480
  ] })
2441
2481
  ] }) });
2442
2482
  }
2443
- function TableRowsPanel({ table, onTableChanged }) {
2483
+ function TableRowsPanel({
2484
+ table,
2485
+ onTableChanged,
2486
+ onEditTable,
2487
+ onDeleteTable,
2488
+ deletingTable
2489
+ }) {
2444
2490
  const tr = useTr();
2445
- const { rows, total, loading, error, refetch } = useDataTableRows(table.id);
2491
+ const [page, setPage] = useState(0);
2492
+ const [pageSize, setPageSize] = useState(DEFAULT_ROW_PAGE_SIZE);
2493
+ const { rows, total, loading, error, refetch } = useDataTableRows(table.id, {
2494
+ page,
2495
+ pageSize
2496
+ });
2446
2497
  const { deleteDataTableRows, loading: deleting } = useDeleteDataTableRows();
2447
2498
  const { insertDataTableRow } = useInsertDataTableRow();
2448
2499
  const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
2449
2500
  const [rowDialogOpen, setRowDialogOpen] = useState(false);
2450
2501
  const [editingRow, setEditingRow] = useState(null);
2502
+ const [exporting, setExporting] = useState(false);
2503
+ const [importProgress, setImportProgress] = useState(null);
2451
2504
  const fileInputRef = useRef(null);
2505
+ const pages = pageCount(total, pageSize);
2506
+ const range = pageRange(page, pageSize, rows.length);
2507
+ useEffect(() => {
2508
+ if (loading) return;
2509
+ const clamped = clampPage(page, total, pageSize);
2510
+ if (clamped !== page) setPage(clamped);
2511
+ }, [loading, page, pageSize, total]);
2452
2512
  useEffect(() => {
2453
2513
  setSelectedIds((current) => {
2454
2514
  const visible = new Set(rows.map((row) => row.id));
@@ -2488,43 +2548,68 @@ function TableRowsPanel({ table, onTableChanged }) {
2488
2548
  );
2489
2549
  }
2490
2550
  }, [deleteDataTableRows, onTableChanged, refetch, selectedIds, table.id, tr]);
2491
- const handleExport = useCallback(() => {
2492
- const csv = rowsToCsv(table.columns, rows);
2493
- const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
2494
- const url = URL.createObjectURL(blob);
2495
- const anchor = document.createElement("a");
2496
- anchor.href = url;
2497
- anchor.download = `${table.name}.csv`;
2498
- anchor.click();
2499
- URL.revokeObjectURL(url);
2500
- toast.success(
2501
- tr("fluidgrids.dataTables.exported", "Visible rows exported to CSV")
2502
- );
2503
- }, [rows, table.columns, table.name, tr]);
2551
+ const handleExport = useCallback(async () => {
2552
+ setExporting(true);
2553
+ try {
2554
+ const allRows = await fetchAllDataTableRows(table.id);
2555
+ const csv = rowsToCsv(table.columns, allRows);
2556
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
2557
+ const url = URL.createObjectURL(blob);
2558
+ const anchor = document.createElement("a");
2559
+ anchor.href = url;
2560
+ anchor.download = `${table.name}.csv`;
2561
+ anchor.click();
2562
+ URL.revokeObjectURL(url);
2563
+ toast.success(
2564
+ tr(
2565
+ "fluidgrids.dataTables.exported",
2566
+ `${allRows.length} row(s) exported to CSV`
2567
+ )
2568
+ );
2569
+ } catch (error2) {
2570
+ toast.error(
2571
+ describeOperationError(
2572
+ error2,
2573
+ tr("fluidgrids.dataTables.exportFailed", "Failed to export CSV")
2574
+ )
2575
+ );
2576
+ } finally {
2577
+ setExporting(false);
2578
+ }
2579
+ }, [table.columns, table.id, table.name, tr]);
2504
2580
  const handleImport = useCallback(
2505
2581
  async (event) => {
2506
2582
  const file = event.target.files?.[0];
2507
2583
  event.target.value = "";
2508
2584
  if (!file) return;
2585
+ let inserted = 0;
2509
2586
  try {
2510
2587
  const importedRows = csvToRows(await file.text(), table.columns);
2511
- let inserted = 0;
2588
+ setImportProgress({ done: 0, total: importedRows.length });
2512
2589
  for (const data of importedRows) {
2513
2590
  await insertDataTableRow(table.id, data);
2514
2591
  inserted += 1;
2592
+ setImportProgress({ done: inserted, total: importedRows.length });
2515
2593
  }
2516
2594
  toast.success(
2517
2595
  tr("fluidgrids.dataTables.imported", `${inserted} row(s) imported`)
2518
2596
  );
2519
- await refetch();
2520
- onTableChanged();
2521
2597
  } catch (error2) {
2598
+ const reason = describeOperationError(
2599
+ error2,
2600
+ tr("fluidgrids.dataTables.importFailed", "Failed to import CSV")
2601
+ );
2522
2602
  toast.error(
2523
- describeOperationError(
2524
- error2,
2525
- tr("fluidgrids.dataTables.importFailed", "Failed to import CSV")
2526
- )
2603
+ inserted > 0 ? tr(
2604
+ "fluidgrids.dataTables.importPartial",
2605
+ `Import stopped after ${inserted} row(s): ${reason}`
2606
+ ) : reason
2527
2607
  );
2608
+ } finally {
2609
+ setImportProgress(null);
2610
+ setPage(0);
2611
+ await refetch();
2612
+ onTableChanged();
2528
2613
  }
2529
2614
  },
2530
2615
  [insertDataTableRow, onTableChanged, refetch, table.columns, table.id, tr]
@@ -2538,14 +2623,14 @@ function TableRowsPanel({ table, onTableChanged }) {
2538
2623
  setRowDialogOpen(true);
2539
2624
  }, []);
2540
2625
  return /* @__PURE__ */ jsxs(Card, { className: "min-w-0 border-border-secondary", children: [
2541
- /* @__PURE__ */ jsxs(CardHeader, { className: "gap-3 border-b border-border-secondary p-4 sm:p-5", children: [
2542
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between", children: [
2543
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
2626
+ /* @__PURE__ */ jsxs(CardHeader, { className: "gap-4 border-b border-border-secondary p-4 sm:p-5", children: [
2627
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
2628
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 basis-64", children: [
2544
2629
  /* @__PURE__ */ jsxs(CardTitle, { className: "flex items-center gap-2 text-lg", children: [
2545
2630
  /* @__PURE__ */ jsx(
2546
2631
  Table2,
2547
2632
  {
2548
- className: "size-5 text-text-secondary",
2633
+ className: "size-5 shrink-0 text-text-secondary",
2549
2634
  "aria-hidden": "true"
2550
2635
  }
2551
2636
  ),
@@ -2553,20 +2638,95 @@ function TableRowsPanel({ table, onTableChanged }) {
2553
2638
  ] }),
2554
2639
  /* @__PURE__ */ jsx(CardDescription, { className: "mt-1", children: table.description || tr("fluidgrids.dataTables.noDescription", "No description") })
2555
2640
  ] }),
2556
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2557
- /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: openNewRow, children: [
2558
- /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2559
- tr("fluidgrids.dataTables.addRow", "Add row")
2641
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
2642
+ /* @__PURE__ */ jsxs(
2643
+ Button,
2644
+ {
2645
+ variant: "outline",
2646
+ size: "sm",
2647
+ className: "whitespace-nowrap",
2648
+ onClick: onEditTable,
2649
+ children: [
2650
+ /* @__PURE__ */ jsx(Pencil, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2651
+ tr("fluidgrids.dataTables.editTable", "Edit table")
2652
+ ]
2653
+ }
2654
+ ),
2655
+ /* @__PURE__ */ jsxs(
2656
+ Button,
2657
+ {
2658
+ variant: "outline",
2659
+ size: "sm",
2660
+ className: "whitespace-nowrap",
2661
+ onClick: onDeleteTable,
2662
+ disabled: deletingTable,
2663
+ children: [
2664
+ /* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2665
+ deletingTable ? tr("fluidgrids.dataTables.deleting", "Deleting\u2026") : tr("fluidgrids.dataTables.deleteTable", "Delete table")
2666
+ ]
2667
+ }
2668
+ )
2669
+ ] })
2670
+ ] }),
2671
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
2672
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary", children: [
2673
+ /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
2674
+ total,
2675
+ " ",
2676
+ tr("fluidgrids.dataTables.rows", "rows")
2677
+ ] }),
2678
+ /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
2679
+ table.columns.length,
2680
+ " ",
2681
+ tr("fluidgrids.dataTables.columns", "columns")
2560
2682
  ] }),
2683
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: formatBytes(table.sizeBytes) }),
2684
+ selectedIds.size > 0 && /* @__PURE__ */ jsxs(
2685
+ Button,
2686
+ {
2687
+ variant: "outline",
2688
+ size: "sm",
2689
+ className: "whitespace-nowrap",
2690
+ onClick: handleDeleteRows,
2691
+ disabled: deleting,
2692
+ children: [
2693
+ /* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2694
+ deleting ? tr("fluidgrids.dataTables.deleting", "Deleting\u2026") : tr(
2695
+ "fluidgrids.dataTables.deleteSelected",
2696
+ `Delete ${selectedIds.size} selected`
2697
+ )
2698
+ ]
2699
+ }
2700
+ )
2701
+ ] }),
2702
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2703
+ /* @__PURE__ */ jsxs(
2704
+ Button,
2705
+ {
2706
+ size: "sm",
2707
+ className: "whitespace-nowrap",
2708
+ onClick: openNewRow,
2709
+ disabled: importProgress !== null,
2710
+ children: [
2711
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2712
+ tr("fluidgrids.dataTables.addRow", "Add row")
2713
+ ]
2714
+ }
2715
+ ),
2561
2716
  /* @__PURE__ */ jsxs(
2562
2717
  Button,
2563
2718
  {
2564
2719
  variant: "outline",
2565
2720
  size: "sm",
2721
+ className: "whitespace-nowrap",
2566
2722
  onClick: () => fileInputRef.current?.click(),
2723
+ disabled: importProgress !== null,
2567
2724
  children: [
2568
2725
  /* @__PURE__ */ jsx(ArrowUpFromLine, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2569
- tr("fluidgrids.dataTables.import", "Import CSV")
2726
+ importProgress ? tr(
2727
+ "fluidgrids.dataTables.importing",
2728
+ `Importing ${importProgress.done} / ${importProgress.total}\u2026`
2729
+ ) : tr("fluidgrids.dataTables.import", "Import CSV")
2570
2730
  ]
2571
2731
  }
2572
2732
  ),
@@ -2585,44 +2745,16 @@ function TableRowsPanel({ table, onTableChanged }) {
2585
2745
  {
2586
2746
  variant: "outline",
2587
2747
  size: "sm",
2748
+ className: "whitespace-nowrap",
2588
2749
  onClick: handleExport,
2589
- disabled: rows.length === 0,
2750
+ disabled: total === 0 || exporting,
2590
2751
  children: [
2591
2752
  /* @__PURE__ */ jsx(ArrowDownToLine, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2592
- tr("fluidgrids.dataTables.export", "Export CSV")
2753
+ exporting ? tr("fluidgrids.dataTables.exporting", "Exporting\u2026") : tr("fluidgrids.dataTables.export", "Export CSV")
2593
2754
  ]
2594
2755
  }
2595
2756
  )
2596
2757
  ] })
2597
- ] }),
2598
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary", children: [
2599
- /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
2600
- total,
2601
- " ",
2602
- tr("fluidgrids.dataTables.rows", "rows")
2603
- ] }),
2604
- /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
2605
- table.columns.length,
2606
- " ",
2607
- tr("fluidgrids.dataTables.columns", "columns")
2608
- ] }),
2609
- /* @__PURE__ */ jsx(Badge, { variant: "outline", children: formatBytes(table.sizeBytes) }),
2610
- selectedIds.size > 0 && /* @__PURE__ */ jsxs(
2611
- Button,
2612
- {
2613
- variant: "outline",
2614
- size: "sm",
2615
- onClick: handleDeleteRows,
2616
- disabled: deleting,
2617
- children: [
2618
- /* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2619
- deleting ? tr("fluidgrids.dataTables.deleting", "Deleting\u2026") : tr(
2620
- "fluidgrids.dataTables.deleteSelected",
2621
- `Delete ${selectedIds.size} selected`
2622
- )
2623
- ]
2624
- }
2625
- )
2626
2758
  ] })
2627
2759
  ] }),
2628
2760
  /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
@@ -2637,7 +2769,7 @@ function TableRowsPanel({ table, onTableChanged }) {
2637
2769
  onChange: toggleAll,
2638
2770
  "aria-label": tr(
2639
2771
  "fluidgrids.dataTables.selectAll",
2640
- "Select all rows"
2772
+ "Select all rows on this page"
2641
2773
  )
2642
2774
  }
2643
2775
  ) }),
@@ -2710,10 +2842,64 @@ function TableRowsPanel({ table, onTableChanged }) {
2710
2842
  ] }, row.id))
2711
2843
  ] })
2712
2844
  ] }) }),
2713
- total > rows.length && !loading && /* @__PURE__ */ jsx("p", { className: "border-t border-border-secondary px-4 py-3 text-xs text-text-secondary", children: tr(
2714
- "fluidgrids.dataTables.rowLimitNote",
2715
- "Showing the first 5,000 rows. Use workflow filters for larger tables."
2716
- ) })
2845
+ total > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t border-border-secondary px-4 py-3 text-xs text-text-secondary", children: [
2846
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: tr(
2847
+ "fluidgrids.dataTables.showingRange",
2848
+ `Showing ${range.from}\u2013${range.to} of ${total}`
2849
+ ) }),
2850
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
2851
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2", children: [
2852
+ /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.rowsPerPage", "Rows per page") }),
2853
+ /* @__PURE__ */ jsx(
2854
+ "select",
2855
+ {
2856
+ className: "h-8 rounded-md border border-input bg-background px-2 text-xs text-text-primary",
2857
+ value: pageSize,
2858
+ onChange: (event) => {
2859
+ setPageSize(Number(event.target.value));
2860
+ setPage(0);
2861
+ },
2862
+ children: ROW_PAGE_SIZES.map((size) => /* @__PURE__ */ jsx("option", { value: size, children: size }, size))
2863
+ }
2864
+ )
2865
+ ] }),
2866
+ /* @__PURE__ */ jsx("span", { children: tr(
2867
+ "fluidgrids.dataTables.pageOf",
2868
+ `Page ${page + 1} of ${pages}`
2869
+ ) }),
2870
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2871
+ /* @__PURE__ */ jsx(
2872
+ Button,
2873
+ {
2874
+ variant: "outline",
2875
+ size: "icon",
2876
+ className: "size-8",
2877
+ onClick: () => setPage((current) => Math.max(current - 1, 0)),
2878
+ disabled: page === 0 || loading,
2879
+ title: tr("fluidgrids.dataTables.prevPage", "Previous page"),
2880
+ "aria-label": tr(
2881
+ "fluidgrids.dataTables.prevPage",
2882
+ "Previous page"
2883
+ ),
2884
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4", "aria-hidden": "true" })
2885
+ }
2886
+ ),
2887
+ /* @__PURE__ */ jsx(
2888
+ Button,
2889
+ {
2890
+ variant: "outline",
2891
+ size: "icon",
2892
+ className: "size-8",
2893
+ onClick: () => setPage((current) => Math.min(current + 1, pages - 1)),
2894
+ disabled: page >= pages - 1 || loading,
2895
+ title: tr("fluidgrids.dataTables.nextPage", "Next page"),
2896
+ "aria-label": tr("fluidgrids.dataTables.nextPage", "Next page"),
2897
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4", "aria-hidden": "true" })
2898
+ }
2899
+ )
2900
+ ] })
2901
+ ] })
2902
+ ] })
2717
2903
  ] }),
2718
2904
  /* @__PURE__ */ jsx(
2719
2905
  RowFormDialog,
@@ -2806,10 +2992,16 @@ function DataTablesPage() {
2806
2992
  tr("fluidgrids.dataTables.workflowData", "Workflow data")
2807
2993
  ] }),
2808
2994
  /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-text-primary sm:text-3xl", children: tr("fluidgrids.dataTables.pageTitle", "Data Tables") }),
2809
- /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-2xl text-text-secondary", children: tr(
2810
- "fluidgrids.dataTables.pageSubtitle",
2811
- "Store structured workspace data that your workflows can read and write across runs."
2812
- ) })
2995
+ /* @__PURE__ */ jsx(
2996
+ PagePurpose,
2997
+ {
2998
+ icon: /* @__PURE__ */ jsx(Database, { className: "size-4", "aria-hidden": "true" }),
2999
+ children: tr(
3000
+ "fluidgrids.dataTables.pageSubtitle",
3001
+ "Use Data Tables as lightweight persistent storage for workflow state, lookups, and records shared across runs."
3002
+ )
3003
+ }
3004
+ )
2813
3005
  ] }),
2814
3006
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
2815
3007
  /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => navigate("../list"), children: [
@@ -2831,6 +3023,59 @@ function DataTablesPage() {
2831
3023
  )
2832
3024
  ] })
2833
3025
  ] }),
3026
+ /* @__PURE__ */ jsx(
3027
+ NextSteps,
3028
+ {
3029
+ storageKey: "data-tables",
3030
+ title: tr(
3031
+ "fluidgrids.dataTables.nextStepsTitle",
3032
+ "How to use Data Tables"
3033
+ ),
3034
+ steps: [
3035
+ {
3036
+ id: "create",
3037
+ label: tr(
3038
+ "fluidgrids.dataTables.nextStepCreateLabel",
3039
+ "Create a table"
3040
+ ),
3041
+ description: tr(
3042
+ "fluidgrids.dataTables.nextStepCreateDescription",
3043
+ "Define typed columns for the records your workflow needs."
3044
+ ),
3045
+ icon: /* @__PURE__ */ jsx(Plus, { className: "size-4", "aria-hidden": "true" }),
3046
+ onClick: () => {
3047
+ setEditingTable(null);
3048
+ setTableDialogOpen(true);
3049
+ }
3050
+ },
3051
+ {
3052
+ id: "connect",
3053
+ label: tr(
3054
+ "fluidgrids.dataTables.nextStepConnectLabel",
3055
+ "Connect it to a workflow"
3056
+ ),
3057
+ description: tr(
3058
+ "fluidgrids.dataTables.nextStepConnectDescription",
3059
+ "Add a Data Table node to get, insert, update, upsert, or delete rows."
3060
+ ),
3061
+ icon: /* @__PURE__ */ jsx(Table2, { className: "size-4", "aria-hidden": "true" }),
3062
+ onClick: () => navigate("../list")
3063
+ },
3064
+ {
3065
+ id: "reuse",
3066
+ label: tr(
3067
+ "fluidgrids.dataTables.nextStepReuseLabel",
3068
+ "Reuse data across runs"
3069
+ ),
3070
+ description: tr(
3071
+ "fluidgrids.dataTables.nextStepReuseDescription",
3072
+ "Use saved status, cursors, lookups, and deduplication keys on later runs."
3073
+ ),
3074
+ icon: /* @__PURE__ */ jsx(RefreshCw, { className: "size-4", "aria-hidden": "true" })
3075
+ }
3076
+ ]
3077
+ }
3078
+ ),
2834
3079
  /* @__PURE__ */ jsxs("div", { className: "grid gap-5 lg:grid-cols-[minmax(16rem,22rem)_minmax(0,1fr)]", children: [
2835
3080
  /* @__PURE__ */ jsxs(Card, { className: "h-fit border-border-secondary", children: [
2836
3081
  /* @__PURE__ */ jsxs(CardHeader, { className: "gap-3 border-b border-border-secondary p-4", children: [
@@ -2869,13 +3114,38 @@ function DataTablesPage() {
2869
3114
  ] }),
2870
3115
  /* @__PURE__ */ jsxs(CardContent, { className: "p-2", children: [
2871
3116
  loading && /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-text-secondary", children: tr("fluidgrids.dataTables.loading", "Loading tables\u2026") }),
2872
- !loading && tables.length === 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 text-sm text-text-secondary", children: [
2873
- /* @__PURE__ */ jsx("p", { children: tr("fluidgrids.dataTables.empty", "No data tables yet") }),
2874
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs", children: tr(
2875
- "fluidgrids.dataTables.emptyHint",
2876
- "Create a table to give your workflows a shared data store."
2877
- ) })
2878
- ] }),
3117
+ !loading && tables.length === 0 && /* @__PURE__ */ jsx(
3118
+ IllustratedEmptyState,
3119
+ {
3120
+ className: "py-8",
3121
+ illustration: "onboarding",
3122
+ title: tr(
3123
+ "fluidgrids.dataTables.empty",
3124
+ "Create your first Data Table"
3125
+ ),
3126
+ description: tr(
3127
+ "fluidgrids.dataTables.emptyHint",
3128
+ "Give your workflows a shared place to store typed records across runs."
3129
+ ),
3130
+ action: /* @__PURE__ */ jsxs(
3131
+ Button,
3132
+ {
3133
+ size: "sm",
3134
+ onClick: () => {
3135
+ setEditingTable(null);
3136
+ setTableDialogOpen(true);
3137
+ },
3138
+ children: [
3139
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }),
3140
+ tr(
3141
+ "fluidgrids.dataTables.createFirstTable",
3142
+ "Create a table"
3143
+ )
3144
+ ]
3145
+ }
3146
+ )
3147
+ }
3148
+ ),
2879
3149
  !loading && tables.map((table) => /* @__PURE__ */ jsxs(
2880
3150
  "button",
2881
3151
  {
@@ -2941,39 +3211,20 @@ function DataTablesPage() {
2941
3211
  "Choose a table to view and edit its rows, or create your first table."
2942
3212
  ) })
2943
3213
  ] }) }),
2944
- selectedTable && /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-3", children: [
2945
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [
2946
- /* @__PURE__ */ jsxs(
2947
- Button,
2948
- {
2949
- variant: "outline",
2950
- size: "sm",
2951
- onClick: () => {
2952
- setEditingTable(selectedTable);
2953
- setTableDialogOpen(true);
2954
- },
2955
- children: [
2956
- /* @__PURE__ */ jsx(Pencil, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2957
- tr("fluidgrids.dataTables.editTable", "Edit table")
2958
- ]
2959
- }
2960
- ),
2961
- /* @__PURE__ */ jsxs(
2962
- Button,
2963
- {
2964
- variant: "outline",
2965
- size: "sm",
2966
- onClick: handleDeleteTable,
2967
- disabled: deleting,
2968
- children: [
2969
- /* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 size-4", "aria-hidden": "true" }),
2970
- deleting ? tr("fluidgrids.dataTables.deleting", "Deleting\u2026") : tr("fluidgrids.dataTables.deleteTable", "Delete table")
2971
- ]
2972
- }
2973
- )
2974
- ] }),
2975
- /* @__PURE__ */ jsx(TableRowsPanel, { table: selectedTable, onTableChanged: refresh })
2976
- ] })
3214
+ selectedTable && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: /* @__PURE__ */ jsx(
3215
+ TableRowsPanel,
3216
+ {
3217
+ table: selectedTable,
3218
+ onTableChanged: refresh,
3219
+ onEditTable: () => {
3220
+ setEditingTable(selectedTable);
3221
+ setTableDialogOpen(true);
3222
+ },
3223
+ onDeleteTable: handleDeleteTable,
3224
+ deletingTable: deleting
3225
+ },
3226
+ selectedTable.id
3227
+ ) })
2977
3228
  ] })
2978
3229
  ] }),
2979
3230
  /* @__PURE__ */ jsx(