stagent 0.6.3 → 0.8.0

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 (139) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +226 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -1
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/projects/[id]/route.ts +37 -0
  17. package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
  18. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  19. package/src/app/api/snapshots/[id]/route.ts +44 -0
  20. package/src/app/api/snapshots/route.ts +54 -0
  21. package/src/app/api/snapshots/settings/route.ts +67 -0
  22. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  23. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  24. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  25. package/src/app/api/tables/[id]/export/route.ts +94 -0
  26. package/src/app/api/tables/[id]/history/route.ts +15 -0
  27. package/src/app/api/tables/[id]/import/route.ts +111 -0
  28. package/src/app/api/tables/[id]/route.ts +86 -0
  29. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  30. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  31. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  32. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  33. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  34. package/src/app/api/tables/route.ts +65 -0
  35. package/src/app/api/tables/templates/route.ts +92 -0
  36. package/src/app/globals.css +14 -0
  37. package/src/app/settings/page.tsx +2 -0
  38. package/src/app/tables/[id]/page.tsx +67 -0
  39. package/src/app/tables/page.tsx +21 -0
  40. package/src/app/tables/templates/page.tsx +19 -0
  41. package/src/components/book/book-reader.tsx +62 -9
  42. package/src/components/book/content-blocks.tsx +6 -1
  43. package/src/components/chat/chat-table-result.tsx +139 -0
  44. package/src/components/documents/document-browser.tsx +1 -1
  45. package/src/components/projects/project-form-sheet.tsx +3 -27
  46. package/src/components/schedules/schedule-form.tsx +5 -27
  47. package/src/components/settings/data-management-section.tsx +17 -12
  48. package/src/components/settings/database-snapshots-section.tsx +469 -0
  49. package/src/components/shared/app-sidebar.tsx +2 -0
  50. package/src/components/shared/document-picker-sheet.tsx +214 -11
  51. package/src/components/tables/table-browser.tsx +234 -0
  52. package/src/components/tables/table-cell-editor.tsx +226 -0
  53. package/src/components/tables/table-chart-builder.tsx +288 -0
  54. package/src/components/tables/table-chart-view.tsx +146 -0
  55. package/src/components/tables/table-column-header.tsx +103 -0
  56. package/src/components/tables/table-column-sheet.tsx +331 -0
  57. package/src/components/tables/table-create-sheet.tsx +240 -0
  58. package/src/components/tables/table-detail-sheet.tsx +144 -0
  59. package/src/components/tables/table-detail-tabs.tsx +278 -0
  60. package/src/components/tables/table-grid.tsx +61 -0
  61. package/src/components/tables/table-history-tab.tsx +148 -0
  62. package/src/components/tables/table-import-wizard.tsx +542 -0
  63. package/src/components/tables/table-list-table.tsx +95 -0
  64. package/src/components/tables/table-relation-combobox.tsx +217 -0
  65. package/src/components/tables/table-row-sheet.tsx +271 -0
  66. package/src/components/tables/table-spreadsheet.tsx +394 -0
  67. package/src/components/tables/table-template-gallery.tsx +162 -0
  68. package/src/components/tables/table-template-preview.tsx +219 -0
  69. package/src/components/tables/table-toolbar.tsx +79 -0
  70. package/src/components/tables/table-triggers-tab.tsx +446 -0
  71. package/src/components/tables/types.ts +6 -0
  72. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  73. package/src/components/tables/utils.ts +29 -0
  74. package/src/components/tasks/task-create-panel.tsx +5 -31
  75. package/src/components/tasks/task-edit-dialog.tsx +5 -27
  76. package/src/components/workflows/workflow-form-view.tsx +11 -35
  77. package/src/components/workflows/workflow-status-view.tsx +1 -1
  78. package/src/instrumentation.ts +3 -0
  79. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  80. package/src/lib/agents/claude-agent.ts +3 -1
  81. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +23 -0
  82. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
  83. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
  84. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  85. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  86. package/src/lib/book/chapter-generator.ts +81 -5
  87. package/src/lib/book/chapter-mapping.ts +58 -24
  88. package/src/lib/book/content.ts +83 -47
  89. package/src/lib/book/markdown-parser.ts +1 -1
  90. package/src/lib/book/reading-paths.ts +8 -8
  91. package/src/lib/book/types.ts +1 -1
  92. package/src/lib/book/update-detector.ts +4 -1
  93. package/src/lib/chat/stagent-tools.ts +2 -0
  94. package/src/lib/chat/tool-catalog.ts +34 -0
  95. package/src/lib/chat/tools/table-tools.ts +955 -0
  96. package/src/lib/chat/tools/workflow-tools.ts +9 -1
  97. package/src/lib/constants/table-status.ts +68 -0
  98. package/src/lib/data/__tests__/clear.test.ts +1 -1
  99. package/src/lib/data/clear.ts +45 -0
  100. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  101. package/src/lib/data/seed-data/conversations.ts +350 -42
  102. package/src/lib/data/seed-data/documents.ts +564 -591
  103. package/src/lib/data/seed-data/learned-context.ts +101 -22
  104. package/src/lib/data/seed-data/notifications.ts +344 -70
  105. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  106. package/src/lib/data/seed-data/profiles.ts +144 -46
  107. package/src/lib/data/seed-data/projects.ts +50 -18
  108. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  109. package/src/lib/data/seed-data/schedules.ts +208 -41
  110. package/src/lib/data/seed-data/table-templates.ts +234 -0
  111. package/src/lib/data/seed-data/tasks.ts +614 -116
  112. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  113. package/src/lib/data/seed-data/user-tables.ts +203 -0
  114. package/src/lib/data/seed-data/views.ts +52 -7
  115. package/src/lib/data/seed-data/workflows.ts +231 -84
  116. package/src/lib/data/seed.ts +55 -14
  117. package/src/lib/data/tables.ts +417 -0
  118. package/src/lib/db/bootstrap.ts +227 -0
  119. package/src/lib/db/index.ts +9 -0
  120. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  121. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  122. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  123. package/src/lib/db/schema.ts +368 -0
  124. package/src/lib/snapshots/auto-backup.ts +132 -0
  125. package/src/lib/snapshots/retention.ts +64 -0
  126. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  127. package/src/lib/tables/computed.ts +61 -0
  128. package/src/lib/tables/context-builder.ts +139 -0
  129. package/src/lib/tables/formula-engine.ts +415 -0
  130. package/src/lib/tables/history.ts +115 -0
  131. package/src/lib/tables/import.ts +343 -0
  132. package/src/lib/tables/query-builder.ts +152 -0
  133. package/src/lib/tables/trigger-evaluator.ts +146 -0
  134. package/src/lib/tables/types.ts +141 -0
  135. package/src/lib/tables/validation.ts +119 -0
  136. package/src/lib/utils/stagent-paths.ts +20 -0
  137. package/src/lib/workflows/types.ts +1 -1
  138. package/tsconfig.json +3 -1
  139. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetHeader,
10
+ SheetTitle,
11
+ SheetFooter,
12
+ } from "@/components/ui/sheet";
13
+ import { ExternalLink, Trash2 } from "lucide-react";
14
+ import { toast } from "sonner";
15
+ import { tableSourceVariant } from "@/lib/constants/table-status";
16
+ import { formatRowCount, formatColumnCount } from "./utils";
17
+ import type { TableWithRelations } from "./types";
18
+ import type { ColumnDef } from "@/lib/tables/types";
19
+
20
+ interface TableDetailSheetProps {
21
+ table: TableWithRelations;
22
+ open: boolean;
23
+ onOpenChange: (open: boolean) => void;
24
+ onDeleted: () => void;
25
+ }
26
+
27
+ export function TableDetailSheet({
28
+ table,
29
+ open,
30
+ onOpenChange,
31
+ onDeleted,
32
+ }: TableDetailSheetProps) {
33
+ const router = useRouter();
34
+
35
+ let columns: ColumnDef[] = [];
36
+ try {
37
+ columns = JSON.parse(table.columnSchema) as ColumnDef[];
38
+ } catch {
39
+ // Invalid schema
40
+ }
41
+
42
+ async function handleDelete() {
43
+ try {
44
+ const res = await fetch(`/api/tables/${table.id}`, {
45
+ method: "DELETE",
46
+ });
47
+ if (res.ok) {
48
+ toast.success("Table deleted");
49
+ onOpenChange(false);
50
+ onDeleted();
51
+ } else {
52
+ toast.error("Failed to delete table");
53
+ }
54
+ } catch {
55
+ toast.error("Failed to delete table");
56
+ }
57
+ }
58
+
59
+ return (
60
+ <Sheet open={open} onOpenChange={onOpenChange}>
61
+ <SheetContent side="right" className="w-[420px] sm:max-w-[420px]">
62
+ <SheetHeader>
63
+ <SheetTitle>{table.name}</SheetTitle>
64
+ </SheetHeader>
65
+
66
+ <div className="px-6 pb-6 space-y-4 overflow-y-auto">
67
+ {table.description && (
68
+ <p className="text-sm text-muted-foreground">
69
+ {table.description}
70
+ </p>
71
+ )}
72
+
73
+ <div className="flex items-center gap-2 text-sm">
74
+ <Badge variant={tableSourceVariant[table.source]}>
75
+ {table.source}
76
+ </Badge>
77
+ <span className="text-muted-foreground">
78
+ {formatColumnCount(table.columnCount)}
79
+ </span>
80
+ <span className="text-muted-foreground">
81
+ {formatRowCount(table.rowCount)}
82
+ </span>
83
+ </div>
84
+
85
+ {table.projectName && (
86
+ <div className="text-sm">
87
+ <span className="text-muted-foreground">Project: </span>
88
+ {table.projectName}
89
+ </div>
90
+ )}
91
+
92
+ {columns.length > 0 && (
93
+ <div className="space-y-1">
94
+ <h4 className="text-sm font-medium">Columns</h4>
95
+ <div className="rounded-md border divide-y">
96
+ {columns.map((col) => (
97
+ <div
98
+ key={col.name}
99
+ className="flex items-center justify-between px-3 py-2 text-sm"
100
+ >
101
+ <span>{col.displayName}</span>
102
+ <Badge variant="outline" className="text-xs">
103
+ {col.dataType}
104
+ </Badge>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ )}
110
+
111
+ <div className="text-xs text-muted-foreground">
112
+ Created{" "}
113
+ {table.createdAt
114
+ ? new Date(table.createdAt).toLocaleDateString()
115
+ : "—"}
116
+ {" · "}
117
+ Updated{" "}
118
+ {table.updatedAt
119
+ ? new Date(table.updatedAt).toLocaleDateString()
120
+ : "—"}
121
+ </div>
122
+ </div>
123
+
124
+ <SheetFooter className="px-6">
125
+ <Button
126
+ variant="destructive"
127
+ size="sm"
128
+ onClick={handleDelete}
129
+ >
130
+ <Trash2 className="h-4 w-4 mr-1" />
131
+ Delete
132
+ </Button>
133
+ <Button
134
+ size="sm"
135
+ onClick={() => router.push(`/tables/${table.id}`)}
136
+ >
137
+ <ExternalLink className="h-4 w-4 mr-1" />
138
+ Open
139
+ </Button>
140
+ </SheetFooter>
141
+ </SheetContent>
142
+ </Sheet>
143
+ );
144
+ }
@@ -0,0 +1,278 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import {
5
+ Tabs,
6
+ TabsContent,
7
+ TabsList,
8
+ TabsTrigger,
9
+ } from "@/components/ui/tabs";
10
+ import { Badge } from "@/components/ui/badge";
11
+ import { TableSpreadsheet } from "./table-spreadsheet";
12
+ import { TableTriggersTab } from "./table-triggers-tab";
13
+ import { TableHistoryTab } from "./table-history-tab";
14
+ import { TableChartBuilder, type EditChartData } from "./table-chart-builder";
15
+ import { TableChartView } from "./table-chart-view";
16
+ import { Button } from "@/components/ui/button";
17
+ import { BarChart3, Pencil, Plus, Trash2 } from "lucide-react";
18
+ import { EmptyState } from "@/components/shared/empty-state";
19
+ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
20
+ import { toast } from "sonner";
21
+ import { tableSourceVariant } from "@/lib/constants/table-status";
22
+ import type { ColumnDef } from "@/lib/tables/types";
23
+ import type { UserTableRowRow } from "@/lib/db/schema";
24
+
25
+ interface ChartView {
26
+ id: string;
27
+ name: string;
28
+ config: {
29
+ type: "bar" | "line" | "pie" | "scatter";
30
+ xColumn: string;
31
+ yColumn?: string;
32
+ aggregation?: "sum" | "avg" | "count" | "min" | "max";
33
+ };
34
+ }
35
+
36
+ interface TableMeta {
37
+ source: string;
38
+ projectName: string | null;
39
+ rowCount: number;
40
+ createdAt: string | null;
41
+ updatedAt: string | null;
42
+ }
43
+
44
+ interface TableDetailTabsProps {
45
+ tableId: string;
46
+ columns: ColumnDef[];
47
+ initialRows: UserTableRowRow[];
48
+ tableMeta?: TableMeta;
49
+ }
50
+
51
+ export function TableDetailTabs({
52
+ tableId,
53
+ columns,
54
+ initialRows,
55
+ tableMeta,
56
+ }: TableDetailTabsProps) {
57
+ const [charts, setCharts] = useState<ChartView[]>([]);
58
+ const [chartBuilderOpen, setChartBuilderOpen] = useState(false);
59
+ const [editChart, setEditChart] = useState<EditChartData | null>(null);
60
+ const [deleteTarget, setDeleteTarget] = useState<ChartView | null>(null);
61
+
62
+ // Parse rows for chart rendering
63
+ const parsedRows = initialRows.map((r) => ({
64
+ data: typeof r.data === "string" ? JSON.parse(r.data) as Record<string, unknown> : r.data as Record<string, unknown>,
65
+ }));
66
+
67
+ const fetchCharts = useCallback(async () => {
68
+ try {
69
+ const res = await fetch(`/api/tables/${tableId}/charts`);
70
+ if (res.ok) setCharts(await res.json());
71
+ } catch { /* silent */ }
72
+ }, [tableId]);
73
+
74
+ useEffect(() => { fetchCharts(); }, [fetchCharts]);
75
+
76
+ function handleEdit(chart: ChartView) {
77
+ setEditChart({
78
+ id: chart.id,
79
+ name: chart.name,
80
+ config: chart.config,
81
+ });
82
+ setChartBuilderOpen(true);
83
+ }
84
+
85
+ async function handleDelete() {
86
+ if (!deleteTarget) return;
87
+ try {
88
+ const res = await fetch(`/api/tables/${tableId}/charts/${deleteTarget.id}`, {
89
+ method: "DELETE",
90
+ });
91
+ if (res.ok || res.status === 204) {
92
+ toast.success(`Chart "${deleteTarget.name}" deleted`);
93
+ fetchCharts();
94
+ } else {
95
+ toast.error("Failed to delete chart");
96
+ }
97
+ } catch {
98
+ toast.error("Failed to delete chart");
99
+ } finally {
100
+ setDeleteTarget(null);
101
+ }
102
+ }
103
+
104
+ function handleBuilderClose(open: boolean) {
105
+ setChartBuilderOpen(open);
106
+ if (!open) setEditChart(null);
107
+ }
108
+
109
+ return (
110
+ <>
111
+ <Tabs defaultValue="data" className="w-full">
112
+ <TabsList>
113
+ <TabsTrigger value="data">Data</TabsTrigger>
114
+ <TabsTrigger value="charts">Charts</TabsTrigger>
115
+ <TabsTrigger value="triggers">Triggers</TabsTrigger>
116
+ <TabsTrigger value="history">History</TabsTrigger>
117
+ {tableMeta && <TabsTrigger value="details">Details</TabsTrigger>}
118
+ </TabsList>
119
+
120
+ <TabsContent value="data" className="mt-4">
121
+ <TableSpreadsheet
122
+ tableId={tableId}
123
+ columns={columns}
124
+ initialRows={initialRows}
125
+ />
126
+ </TabsContent>
127
+
128
+ <TabsContent value="charts" className="mt-4">
129
+ <div className="space-y-4">
130
+ <div className="flex justify-end">
131
+ <Button variant="outline" size="sm" onClick={() => { setEditChart(null); setChartBuilderOpen(true); }}>
132
+ <Plus className="h-4 w-4 mr-1" />
133
+ New Chart
134
+ </Button>
135
+ </div>
136
+
137
+ {charts.length === 0 ? (
138
+ <EmptyState
139
+ icon={BarChart3}
140
+ heading="No charts yet"
141
+ description="Create a chart to visualize your table data."
142
+ action={
143
+ <Button onClick={() => setChartBuilderOpen(true)}>
144
+ <Plus className="h-4 w-4 mr-1" />
145
+ Create Chart
146
+ </Button>
147
+ }
148
+ />
149
+ ) : (
150
+ <div className="grid gap-4 md:grid-cols-2">
151
+ {charts.map((chart) => (
152
+ <div key={chart.id} className="border rounded-lg p-4">
153
+ <div className="flex items-center justify-between mb-2">
154
+ <h4 className="text-sm font-medium truncate">{chart.name}</h4>
155
+ <div className="flex items-center gap-1 shrink-0">
156
+ <Button
157
+ variant="ghost"
158
+ size="icon"
159
+ className="h-7 w-7"
160
+ onClick={() => handleEdit(chart)}
161
+ aria-label={`Edit ${chart.name}`}
162
+ >
163
+ <Pencil className="h-3.5 w-3.5" />
164
+ </Button>
165
+ <Button
166
+ variant="ghost"
167
+ size="icon"
168
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
169
+ onClick={() => setDeleteTarget(chart)}
170
+ aria-label={`Delete ${chart.name}`}
171
+ >
172
+ <Trash2 className="h-3.5 w-3.5" />
173
+ </Button>
174
+ </div>
175
+ </div>
176
+ <TableChartView
177
+ config={chart.config}
178
+ title=""
179
+ rows={parsedRows}
180
+ />
181
+ </div>
182
+ ))}
183
+ </div>
184
+ )}
185
+
186
+ <TableChartBuilder
187
+ tableId={tableId}
188
+ columns={columns.map((c) => ({
189
+ name: c.name,
190
+ displayName: c.displayName,
191
+ dataType: c.dataType,
192
+ }))}
193
+ open={chartBuilderOpen}
194
+ onOpenChange={handleBuilderClose}
195
+ onChartSaved={fetchCharts}
196
+ editChart={editChart}
197
+ />
198
+ </div>
199
+ </TabsContent>
200
+
201
+ <TabsContent value="triggers" className="mt-4">
202
+ <TableTriggersTab tableId={tableId} />
203
+ </TabsContent>
204
+
205
+ <TabsContent value="history" className="mt-4">
206
+ <TableHistoryTab tableId={tableId} />
207
+ </TabsContent>
208
+
209
+ {tableMeta && (
210
+ <TabsContent value="details" className="mt-4">
211
+ <div className="space-y-4 max-w-lg">
212
+ <div className="grid grid-cols-2 gap-y-3 text-sm">
213
+ <span className="text-muted-foreground">Source</span>
214
+ <div>
215
+ <Badge variant={tableSourceVariant[tableMeta.source as keyof typeof tableSourceVariant] ?? "outline"}>
216
+ {tableMeta.source}
217
+ </Badge>
218
+ </div>
219
+
220
+ <span className="text-muted-foreground">Project</span>
221
+ <span>{tableMeta.projectName ?? "—"}</span>
222
+
223
+ <span className="text-muted-foreground">Columns</span>
224
+ <span>{columns.length}</span>
225
+
226
+ <span className="text-muted-foreground">Rows</span>
227
+ <span>{tableMeta.rowCount}</span>
228
+
229
+ <span className="text-muted-foreground">Created</span>
230
+ <span>
231
+ {tableMeta.createdAt
232
+ ? new Date(tableMeta.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
233
+ : "—"}
234
+ </span>
235
+
236
+ <span className="text-muted-foreground">Updated</span>
237
+ <span>
238
+ {tableMeta.updatedAt
239
+ ? new Date(tableMeta.updatedAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
240
+ : "—"}
241
+ </span>
242
+ </div>
243
+
244
+ {columns.length > 0 && (
245
+ <div className="space-y-2">
246
+ <h4 className="text-sm font-medium">Column Schema</h4>
247
+ <div className="rounded-md border divide-y">
248
+ {columns.map((col) => (
249
+ <div
250
+ key={col.name}
251
+ className="flex items-center justify-between px-3 py-2 text-sm"
252
+ >
253
+ <span>{col.displayName}</span>
254
+ <Badge variant="outline" className="text-xs">
255
+ {col.dataType}
256
+ </Badge>
257
+ </div>
258
+ ))}
259
+ </div>
260
+ </div>
261
+ )}
262
+ </div>
263
+ </TabsContent>
264
+ )}
265
+ </Tabs>
266
+
267
+ <ConfirmDialog
268
+ open={!!deleteTarget}
269
+ onOpenChange={(open) => { if (!open) setDeleteTarget(null); }}
270
+ title={`Delete chart "${deleteTarget?.name}"?`}
271
+ description="This will permanently remove this chart. The underlying table data is not affected."
272
+ confirmLabel="Delete Chart"
273
+ onConfirm={handleDelete}
274
+ destructive
275
+ />
276
+ </>
277
+ );
278
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Table2 } from "lucide-react";
6
+ import { tableSourceVariant } from "@/lib/constants/table-status";
7
+ import { formatRowCount, formatColumnCount } from "./utils";
8
+ import type { TableWithRelations } from "./types";
9
+
10
+ interface TableGridProps {
11
+ tables: TableWithRelations[];
12
+ onSelect: (id: string) => void;
13
+ onOpen: (id: string) => void;
14
+ }
15
+
16
+ export function TableGrid({ tables, onSelect, onOpen }: TableGridProps) {
17
+ return (
18
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
19
+ {tables.map((t) => (
20
+ <Card
21
+ key={t.id}
22
+ className="cursor-pointer hover:border-primary/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg"
23
+ tabIndex={0}
24
+ onClick={() => onSelect(t.id)}
25
+ onDoubleClick={() => onOpen(t.id)}
26
+ onKeyDown={(e) => {
27
+ if (e.key === "Enter") onOpen(t.id);
28
+ }}
29
+ >
30
+ <CardHeader className="pb-2">
31
+ <div className="flex items-start justify-between">
32
+ <div className="flex items-center gap-2">
33
+ <Table2 className="h-4 w-4 text-muted-foreground" />
34
+ <CardTitle className="text-sm font-medium">
35
+ {t.name}
36
+ </CardTitle>
37
+ </div>
38
+ <Badge variant={tableSourceVariant[t.source]} className="text-xs">
39
+ {t.source}
40
+ </Badge>
41
+ </div>
42
+ </CardHeader>
43
+ <CardContent>
44
+ {t.description && (
45
+ <p className="text-xs text-muted-foreground mb-2 line-clamp-2">
46
+ {t.description}
47
+ </p>
48
+ )}
49
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
50
+ <span>{formatColumnCount(t.columnCount)}</span>
51
+ <span>{formatRowCount(t.rowCount)}</span>
52
+ {t.projectName && (
53
+ <span className="truncate">{t.projectName}</span>
54
+ )}
55
+ </div>
56
+ </CardContent>
57
+ </Card>
58
+ ))}
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { toast } from "sonner";
7
+ import { History, RotateCcw, User, Bot } from "lucide-react";
8
+ import { EmptyState } from "@/components/shared/empty-state";
9
+
10
+ interface HistoryEntry {
11
+ id: string;
12
+ rowId: string;
13
+ tableId: string;
14
+ previousData: string;
15
+ changedBy: string;
16
+ changeType: "update" | "delete";
17
+ createdAt: string;
18
+ }
19
+
20
+ interface TableHistoryTabProps {
21
+ tableId: string;
22
+ }
23
+
24
+ export function TableHistoryTab({ tableId }: TableHistoryTabProps) {
25
+ const [entries, setEntries] = useState<HistoryEntry[]>([]);
26
+ const [loading, setLoading] = useState(true);
27
+ const [expandedId, setExpandedId] = useState<string | null>(null);
28
+
29
+ const fetchHistory = useCallback(async () => {
30
+ try {
31
+ const res = await fetch(`/api/tables/${tableId}/history?limit=100`);
32
+ if (res.ok) {
33
+ setEntries(await res.json());
34
+ }
35
+ } catch { /* silent */ }
36
+ finally { setLoading(false); }
37
+ }, [tableId]);
38
+
39
+ useEffect(() => { fetchHistory(); }, [fetchHistory]);
40
+
41
+ async function handleRollback(entry: HistoryEntry) {
42
+ try {
43
+ const res = await fetch(`/api/tables/${tableId}/rows/${entry.rowId}/history`, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ historyEntryId: entry.id }),
47
+ });
48
+ if (res.ok) {
49
+ toast.success("Row restored to previous version");
50
+ fetchHistory();
51
+ } else {
52
+ const err = await res.json().catch(() => ({}));
53
+ toast.error(err.error || "Failed to rollback");
54
+ }
55
+ } catch {
56
+ toast.error("Failed to rollback");
57
+ }
58
+ }
59
+
60
+ function formatDate(dateStr: string) {
61
+ try {
62
+ return new Date(dateStr).toLocaleString();
63
+ } catch {
64
+ return dateStr;
65
+ }
66
+ }
67
+
68
+ function formatData(json: string) {
69
+ try {
70
+ return JSON.stringify(JSON.parse(json), null, 2);
71
+ } catch {
72
+ return json;
73
+ }
74
+ }
75
+
76
+ if (loading) {
77
+ return <p className="text-sm text-muted-foreground p-4">Loading history...</p>;
78
+ }
79
+
80
+ if (entries.length === 0) {
81
+ return (
82
+ <EmptyState
83
+ icon={History}
84
+ heading="No history yet"
85
+ description="Row changes will be tracked here as you edit data."
86
+ />
87
+ );
88
+ }
89
+
90
+ return (
91
+ <div className="space-y-2 p-4">
92
+ <p className="text-xs text-muted-foreground mb-3">
93
+ {entries.length} change{entries.length !== 1 ? "s" : ""} recorded
94
+ </p>
95
+ <div className="space-y-1">
96
+ {entries.map((entry) => (
97
+ <div
98
+ key={entry.id}
99
+ className="border rounded-lg p-3 space-y-2"
100
+ >
101
+ <div className="flex items-center justify-between">
102
+ <div className="flex items-center gap-2 text-sm">
103
+ {entry.changedBy === "agent" ? (
104
+ <Bot className="h-3.5 w-3.5 text-muted-foreground" />
105
+ ) : (
106
+ <User className="h-3.5 w-3.5 text-muted-foreground" />
107
+ )}
108
+ <Badge variant={entry.changeType === "delete" ? "destructive" : "secondary"}>
109
+ {entry.changeType}
110
+ </Badge>
111
+ <span className="text-muted-foreground text-xs">
112
+ Row {entry.rowId.slice(0, 8)}...
113
+ </span>
114
+ <span className="text-muted-foreground text-xs">
115
+ {formatDate(entry.createdAt)}
116
+ </span>
117
+ </div>
118
+ <div className="flex items-center gap-1">
119
+ <Button
120
+ variant="ghost"
121
+ size="sm"
122
+ onClick={() => setExpandedId(expandedId === entry.id ? null : entry.id)}
123
+ >
124
+ {expandedId === entry.id ? "Hide" : "View"}
125
+ </Button>
126
+ {entry.changeType !== "delete" && (
127
+ <Button
128
+ variant="outline"
129
+ size="sm"
130
+ onClick={() => handleRollback(entry)}
131
+ >
132
+ <RotateCcw className="h-3 w-3 mr-1" />
133
+ Restore
134
+ </Button>
135
+ )}
136
+ </div>
137
+ </div>
138
+ {expandedId === entry.id && (
139
+ <pre className="text-xs bg-muted p-2 rounded overflow-x-auto max-h-48">
140
+ {formatData(entry.previousData)}
141
+ </pre>
142
+ )}
143
+ </div>
144
+ ))}
145
+ </div>
146
+ </div>
147
+ );
148
+ }