stagent 0.6.3 → 0.7.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.
- package/README.md +21 -2
- package/dist/cli.js +226 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -1
- package/src/app/api/chat/entities/search/route.ts +12 -3
- package/src/app/api/projects/[id]/route.ts +37 -0
- package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- package/src/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- package/src/components/projects/project-form-sheet.tsx +3 -27
- package/src/components/schedules/schedule-form.tsx +5 -27
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +214 -11
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-spreadsheet.tsx +499 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-create-panel.tsx +5 -31
- package/src/components/tasks/task-edit-dialog.tsx +5 -27
- package/src/components/workflows/workflow-form-view.tsx +5 -29
- package/src/components/workflows/workflow-status-view.tsx +1 -1
- package/src/instrumentation.ts +3 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/tool-catalog.ts +34 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +45 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +227 -0
- package/src/lib/db/index.ts +9 -0
- package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +368 -0
- package/src/lib/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/stagent-paths.ts +20 -0
- package/tsconfig.json +3 -1
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -0,0 +1,542 @@
|
|
|
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 { Checkbox } from "@/components/ui/checkbox";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "@/components/ui/select";
|
|
14
|
+
import {
|
|
15
|
+
Sheet,
|
|
16
|
+
SheetContent,
|
|
17
|
+
SheetHeader,
|
|
18
|
+
SheetTitle,
|
|
19
|
+
SheetFooter,
|
|
20
|
+
} from "@/components/ui/sheet";
|
|
21
|
+
import { FileSpreadsheet, Loader2, CheckCircle2, AlertCircle } from "lucide-react";
|
|
22
|
+
import { toast } from "sonner";
|
|
23
|
+
import { COLUMN_DATA_TYPES, columnTypeLabel } from "@/lib/constants/table-status";
|
|
24
|
+
import type { ColumnDataType } from "@/lib/constants/table-status";
|
|
25
|
+
|
|
26
|
+
// ── Types ─────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
interface TableImportWizardProps {
|
|
29
|
+
tableId: string;
|
|
30
|
+
open: boolean;
|
|
31
|
+
onOpenChange: (open: boolean) => void;
|
|
32
|
+
onImported: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DocumentItem {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
fileSize: number;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface InferredColumn {
|
|
44
|
+
name: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
dataType: ColumnDataType;
|
|
47
|
+
config?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface PreviewData {
|
|
51
|
+
headers: string[];
|
|
52
|
+
sampleRows: Record<string, string>[];
|
|
53
|
+
totalRows: number;
|
|
54
|
+
inferredColumns: InferredColumn[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ColumnConfig extends InferredColumn {
|
|
58
|
+
skip: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ImportResult {
|
|
62
|
+
importId: string;
|
|
63
|
+
rowsImported: number;
|
|
64
|
+
rowsSkipped: number;
|
|
65
|
+
errors: Array<{ row: number; error: string }>;
|
|
66
|
+
columns: InferredColumn[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type Step = "select" | "columns" | "results";
|
|
70
|
+
|
|
71
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const IMPORTABLE_MIME_TYPES = [
|
|
74
|
+
"text/csv",
|
|
75
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
76
|
+
"application/vnd.ms-excel",
|
|
77
|
+
"text/tab-separated-values",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function formatSize(bytes: number): string {
|
|
81
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
82
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
83
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mimeLabel(mime: string): string {
|
|
87
|
+
if (mime === "text/csv") return "CSV";
|
|
88
|
+
if (mime === "text/tab-separated-values") return "TSV";
|
|
89
|
+
if (mime.includes("spreadsheetml") || mime.includes("ms-excel")) return "Excel";
|
|
90
|
+
return mime;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Component ─────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export function TableImportWizard({
|
|
96
|
+
tableId,
|
|
97
|
+
open,
|
|
98
|
+
onOpenChange,
|
|
99
|
+
onImported,
|
|
100
|
+
}: TableImportWizardProps) {
|
|
101
|
+
const [step, setStep] = useState<Step>("select");
|
|
102
|
+
const [documents, setDocuments] = useState<DocumentItem[]>([]);
|
|
103
|
+
const [loadingDocs, setLoadingDocs] = useState(false);
|
|
104
|
+
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
|
|
105
|
+
|
|
106
|
+
// Preview state
|
|
107
|
+
const [preview, setPreview] = useState<PreviewData | null>(null);
|
|
108
|
+
const [loadingPreview, setLoadingPreview] = useState(false);
|
|
109
|
+
|
|
110
|
+
// Column config state
|
|
111
|
+
const [columnConfigs, setColumnConfigs] = useState<ColumnConfig[]>([]);
|
|
112
|
+
|
|
113
|
+
// Import state
|
|
114
|
+
const [importing, setImporting] = useState(false);
|
|
115
|
+
const [result, setResult] = useState<ImportResult | null>(null);
|
|
116
|
+
|
|
117
|
+
// ── Load documents ──────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const loadDocuments = useCallback(async () => {
|
|
120
|
+
setLoadingDocs(true);
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch("/api/documents");
|
|
123
|
+
if (!res.ok) throw new Error("Failed to load documents");
|
|
124
|
+
const data = await res.json();
|
|
125
|
+
// Filter to importable mime types
|
|
126
|
+
const filtered = (data as DocumentItem[]).filter((doc) =>
|
|
127
|
+
IMPORTABLE_MIME_TYPES.includes(doc.mimeType)
|
|
128
|
+
);
|
|
129
|
+
setDocuments(filtered);
|
|
130
|
+
} catch {
|
|
131
|
+
toast.error("Failed to load documents");
|
|
132
|
+
} finally {
|
|
133
|
+
setLoadingDocs(false);
|
|
134
|
+
}
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (open) {
|
|
139
|
+
loadDocuments();
|
|
140
|
+
// Reset state when opening
|
|
141
|
+
setStep("select");
|
|
142
|
+
setSelectedDocId(null);
|
|
143
|
+
setPreview(null);
|
|
144
|
+
setColumnConfigs([]);
|
|
145
|
+
setResult(null);
|
|
146
|
+
}
|
|
147
|
+
}, [open, loadDocuments]);
|
|
148
|
+
|
|
149
|
+
// ── Select document and load preview ────────────────────────────────
|
|
150
|
+
|
|
151
|
+
async function handleSelectDocument(docId: string) {
|
|
152
|
+
setSelectedDocId(docId);
|
|
153
|
+
setLoadingPreview(true);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(`/api/tables/${tableId}/import`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ documentId: docId, preview: true }),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const err = await res.json();
|
|
164
|
+
toast.error(err.error || "Failed to preview document");
|
|
165
|
+
setSelectedDocId(null);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const data: PreviewData = await res.json();
|
|
170
|
+
setPreview(data);
|
|
171
|
+
setColumnConfigs(
|
|
172
|
+
data.inferredColumns.map((col) => ({ ...col, skip: false }))
|
|
173
|
+
);
|
|
174
|
+
setStep("columns");
|
|
175
|
+
} catch {
|
|
176
|
+
toast.error("Failed to preview document");
|
|
177
|
+
setSelectedDocId(null);
|
|
178
|
+
} finally {
|
|
179
|
+
setLoadingPreview(false);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Run import ──────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
async function handleImport() {
|
|
186
|
+
if (!selectedDocId) return;
|
|
187
|
+
|
|
188
|
+
setImporting(true);
|
|
189
|
+
try {
|
|
190
|
+
const mapping = columnConfigs.map((col) => ({
|
|
191
|
+
name: col.name,
|
|
192
|
+
displayName: col.displayName,
|
|
193
|
+
dataType: col.dataType,
|
|
194
|
+
skip: col.skip,
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
const res = await fetch(`/api/tables/${tableId}/import`, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "Content-Type": "application/json" },
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
documentId: selectedDocId,
|
|
202
|
+
columnMapping: mapping,
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
const err = await res.json();
|
|
208
|
+
toast.error(err.error || "Import failed");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const data: ImportResult = await res.json();
|
|
213
|
+
setResult(data);
|
|
214
|
+
setStep("results");
|
|
215
|
+
toast.success(`Imported ${data.rowsImported} rows`);
|
|
216
|
+
} catch {
|
|
217
|
+
toast.error("Import failed");
|
|
218
|
+
} finally {
|
|
219
|
+
setImporting(false);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Column config helpers ───────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
function toggleColumn(index: number) {
|
|
226
|
+
setColumnConfigs((prev) =>
|
|
227
|
+
prev.map((col, i) =>
|
|
228
|
+
i === index ? { ...col, skip: !col.skip } : col
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function updateColumnType(index: number, dataType: string) {
|
|
234
|
+
setColumnConfigs((prev) =>
|
|
235
|
+
prev.map((col, i) =>
|
|
236
|
+
i === index ? { ...col, dataType: dataType as ColumnDataType } : col
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Render steps ────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function renderSelectStep() {
|
|
244
|
+
if (loadingDocs) {
|
|
245
|
+
return (
|
|
246
|
+
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
|
247
|
+
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
|
248
|
+
Loading documents...
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (documents.length === 0) {
|
|
254
|
+
return (
|
|
255
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
256
|
+
<FileSpreadsheet className="h-10 w-10 mx-auto mb-3 opacity-40" />
|
|
257
|
+
<p className="text-sm font-medium">No importable documents</p>
|
|
258
|
+
<p className="text-xs mt-1">
|
|
259
|
+
Upload a CSV, TSV, or Excel file first via the Documents page.
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<div className="space-y-1">
|
|
267
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
268
|
+
Select a document to import data from.
|
|
269
|
+
</p>
|
|
270
|
+
{documents.map((doc) => (
|
|
271
|
+
<button
|
|
272
|
+
key={doc.id}
|
|
273
|
+
onClick={() => handleSelectDocument(doc.id)}
|
|
274
|
+
disabled={loadingPreview}
|
|
275
|
+
className={`w-full text-left p-3 rounded-lg border transition-colors
|
|
276
|
+
${selectedDocId === doc.id
|
|
277
|
+
? "border-primary bg-primary/5"
|
|
278
|
+
: "border-border hover:border-primary/40 hover:bg-muted/50"
|
|
279
|
+
}
|
|
280
|
+
${loadingPreview ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
|
281
|
+
`}
|
|
282
|
+
>
|
|
283
|
+
<div className="flex items-center justify-between">
|
|
284
|
+
<div className="min-w-0 flex-1">
|
|
285
|
+
<p className="text-sm font-medium truncate">{doc.name}</p>
|
|
286
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
287
|
+
{formatSize(doc.fileSize)}
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
<Badge variant="secondary" className="ml-2 shrink-0">
|
|
291
|
+
{mimeLabel(doc.mimeType)}
|
|
292
|
+
</Badge>
|
|
293
|
+
</div>
|
|
294
|
+
</button>
|
|
295
|
+
))}
|
|
296
|
+
|
|
297
|
+
{loadingPreview && (
|
|
298
|
+
<div className="flex items-center justify-center py-4 text-muted-foreground">
|
|
299
|
+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
300
|
+
Analyzing document...
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderColumnsStep() {
|
|
308
|
+
if (!preview) return null;
|
|
309
|
+
|
|
310
|
+
const activeCount = columnConfigs.filter((c) => !c.skip).length;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className="space-y-4">
|
|
314
|
+
{/* Preview table */}
|
|
315
|
+
<div>
|
|
316
|
+
<p className="text-sm font-medium mb-2">
|
|
317
|
+
Preview ({preview.totalRows} rows total)
|
|
318
|
+
</p>
|
|
319
|
+
<div className="border rounded-lg overflow-auto max-h-48">
|
|
320
|
+
<table className="w-full text-xs">
|
|
321
|
+
<thead>
|
|
322
|
+
<tr className="border-b bg-muted/50">
|
|
323
|
+
{preview.headers.map((h) => (
|
|
324
|
+
<th
|
|
325
|
+
key={h}
|
|
326
|
+
className="px-2 py-1.5 text-left font-medium whitespace-nowrap"
|
|
327
|
+
>
|
|
328
|
+
{h}
|
|
329
|
+
</th>
|
|
330
|
+
))}
|
|
331
|
+
</tr>
|
|
332
|
+
</thead>
|
|
333
|
+
<tbody>
|
|
334
|
+
{preview.sampleRows.slice(0, 5).map((row, i) => (
|
|
335
|
+
<tr key={i} className="border-b last:border-0">
|
|
336
|
+
{preview.headers.map((h) => (
|
|
337
|
+
<td
|
|
338
|
+
key={h}
|
|
339
|
+
className="px-2 py-1 whitespace-nowrap max-w-[200px] truncate"
|
|
340
|
+
>
|
|
341
|
+
{row[h] ?? ""}
|
|
342
|
+
</td>
|
|
343
|
+
))}
|
|
344
|
+
</tr>
|
|
345
|
+
))}
|
|
346
|
+
</tbody>
|
|
347
|
+
</table>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Column configuration */}
|
|
352
|
+
<div>
|
|
353
|
+
<p className="text-sm font-medium mb-2">
|
|
354
|
+
Columns ({activeCount} of {columnConfigs.length} selected)
|
|
355
|
+
</p>
|
|
356
|
+
<div className="space-y-2">
|
|
357
|
+
{columnConfigs.map((col, i) => (
|
|
358
|
+
<div
|
|
359
|
+
key={col.name}
|
|
360
|
+
className={`flex items-center gap-3 p-2 rounded-lg border ${
|
|
361
|
+
col.skip ? "opacity-50 bg-muted/30" : ""
|
|
362
|
+
}`}
|
|
363
|
+
>
|
|
364
|
+
<Checkbox
|
|
365
|
+
checked={!col.skip}
|
|
366
|
+
onCheckedChange={() => toggleColumn(i)}
|
|
367
|
+
/>
|
|
368
|
+
<span className="text-sm font-medium min-w-[120px] truncate">
|
|
369
|
+
{col.displayName}
|
|
370
|
+
</span>
|
|
371
|
+
<Select
|
|
372
|
+
value={col.dataType}
|
|
373
|
+
onValueChange={(v) => updateColumnType(i, v)}
|
|
374
|
+
disabled={col.skip}
|
|
375
|
+
>
|
|
376
|
+
<SelectTrigger className="w-[120px] h-8">
|
|
377
|
+
<SelectValue />
|
|
378
|
+
</SelectTrigger>
|
|
379
|
+
<SelectContent>
|
|
380
|
+
{COLUMN_DATA_TYPES.filter(
|
|
381
|
+
(dt) => dt !== "relation" && dt !== "computed"
|
|
382
|
+
).map((dt) => (
|
|
383
|
+
<SelectItem key={dt} value={dt}>
|
|
384
|
+
{columnTypeLabel[dt]}
|
|
385
|
+
</SelectItem>
|
|
386
|
+
))}
|
|
387
|
+
</SelectContent>
|
|
388
|
+
</Select>
|
|
389
|
+
{col.config?.options != null && (
|
|
390
|
+
<span className="text-xs text-muted-foreground">
|
|
391
|
+
{(col.config.options as string[]).length} options
|
|
392
|
+
</span>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function renderResultsStep() {
|
|
403
|
+
if (!result) return null;
|
|
404
|
+
|
|
405
|
+
const hasErrors = result.rowsSkipped > 0;
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div className="space-y-4">
|
|
409
|
+
<div className="flex items-center gap-3 py-4">
|
|
410
|
+
{hasErrors ? (
|
|
411
|
+
<AlertCircle className="h-8 w-8 text-amber-500 shrink-0" />
|
|
412
|
+
) : (
|
|
413
|
+
<CheckCircle2 className="h-8 w-8 text-green-600 shrink-0" />
|
|
414
|
+
)}
|
|
415
|
+
<div>
|
|
416
|
+
<p className="text-sm font-medium">
|
|
417
|
+
{hasErrors ? "Import completed with warnings" : "Import successful"}
|
|
418
|
+
</p>
|
|
419
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
420
|
+
{result.rowsImported} rows imported
|
|
421
|
+
{result.rowsSkipped > 0 && `, ${result.rowsSkipped} skipped`}
|
|
422
|
+
</p>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
{/* Summary */}
|
|
427
|
+
<div className="grid grid-cols-2 gap-3">
|
|
428
|
+
<div className="border rounded-lg p-3">
|
|
429
|
+
<p className="text-xs text-muted-foreground">Rows Imported</p>
|
|
430
|
+
<p className="text-lg font-semibold">{result.rowsImported}</p>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="border rounded-lg p-3">
|
|
433
|
+
<p className="text-xs text-muted-foreground">Columns</p>
|
|
434
|
+
<p className="text-lg font-semibold">{result.columns.length}</p>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{/* Errors */}
|
|
439
|
+
{result.errors.length > 0 && (
|
|
440
|
+
<div>
|
|
441
|
+
<p className="text-sm font-medium mb-2 text-amber-600">
|
|
442
|
+
Errors ({result.errors.length})
|
|
443
|
+
</p>
|
|
444
|
+
<div className="border rounded-lg overflow-auto max-h-32">
|
|
445
|
+
<table className="w-full text-xs">
|
|
446
|
+
<thead>
|
|
447
|
+
<tr className="border-b bg-muted/50">
|
|
448
|
+
<th className="px-2 py-1.5 text-left font-medium">Row</th>
|
|
449
|
+
<th className="px-2 py-1.5 text-left font-medium">Error</th>
|
|
450
|
+
</tr>
|
|
451
|
+
</thead>
|
|
452
|
+
<tbody>
|
|
453
|
+
{result.errors.map((err, i) => (
|
|
454
|
+
<tr key={i} className="border-b last:border-0">
|
|
455
|
+
<td className="px-2 py-1 text-muted-foreground">{err.row}</td>
|
|
456
|
+
<td className="px-2 py-1">{err.error}</td>
|
|
457
|
+
</tr>
|
|
458
|
+
))}
|
|
459
|
+
</tbody>
|
|
460
|
+
</table>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Step title + navigation ─────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
const stepTitles: Record<Step, string> = {
|
|
471
|
+
select: "Import Data - Select Document",
|
|
472
|
+
columns: "Import Data - Configure Columns",
|
|
473
|
+
results: "Import Data - Results",
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
function handleClose() {
|
|
477
|
+
if (result) {
|
|
478
|
+
onImported();
|
|
479
|
+
}
|
|
480
|
+
onOpenChange(false);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<Sheet open={open} onOpenChange={handleClose}>
|
|
485
|
+
<SheetContent side="right" className="w-[540px] sm:max-w-[540px]">
|
|
486
|
+
<SheetHeader>
|
|
487
|
+
<SheetTitle>{stepTitles[step]}</SheetTitle>
|
|
488
|
+
</SheetHeader>
|
|
489
|
+
|
|
490
|
+
<div className="px-6 pb-6 space-y-4 overflow-y-auto flex-1">
|
|
491
|
+
{step === "select" && renderSelectStep()}
|
|
492
|
+
{step === "columns" && renderColumnsStep()}
|
|
493
|
+
{step === "results" && renderResultsStep()}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<SheetFooter className="px-6">
|
|
497
|
+
{step === "select" && (
|
|
498
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
499
|
+
Cancel
|
|
500
|
+
</Button>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{step === "columns" && (
|
|
504
|
+
<>
|
|
505
|
+
<Button
|
|
506
|
+
variant="outline"
|
|
507
|
+
onClick={() => {
|
|
508
|
+
setStep("select");
|
|
509
|
+
setSelectedDocId(null);
|
|
510
|
+
setPreview(null);
|
|
511
|
+
}}
|
|
512
|
+
disabled={importing}
|
|
513
|
+
>
|
|
514
|
+
Back
|
|
515
|
+
</Button>
|
|
516
|
+
<Button
|
|
517
|
+
onClick={handleImport}
|
|
518
|
+
disabled={
|
|
519
|
+
importing ||
|
|
520
|
+
columnConfigs.every((c) => c.skip)
|
|
521
|
+
}
|
|
522
|
+
>
|
|
523
|
+
{importing ? (
|
|
524
|
+
<>
|
|
525
|
+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
526
|
+
Importing...
|
|
527
|
+
</>
|
|
528
|
+
) : (
|
|
529
|
+
`Import ${preview?.totalRows ?? 0} Rows`
|
|
530
|
+
)}
|
|
531
|
+
</Button>
|
|
532
|
+
</>
|
|
533
|
+
)}
|
|
534
|
+
|
|
535
|
+
{step === "results" && (
|
|
536
|
+
<Button onClick={handleClose}>Done</Button>
|
|
537
|
+
)}
|
|
538
|
+
</SheetFooter>
|
|
539
|
+
</SheetContent>
|
|
540
|
+
</Sheet>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import {
|
|
6
|
+
Table,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
} from "@/components/ui/table";
|
|
13
|
+
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
14
|
+
import { formatRowCount } from "./utils";
|
|
15
|
+
import type { TableWithRelations } from "./types";
|
|
16
|
+
|
|
17
|
+
interface TableListTableProps {
|
|
18
|
+
tables: TableWithRelations[];
|
|
19
|
+
selected: Set<string>;
|
|
20
|
+
onToggleSelect: (id: string) => void;
|
|
21
|
+
onToggleSelectAll: () => void;
|
|
22
|
+
onSelect: (id: string) => void;
|
|
23
|
+
onOpen: (id: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TableListTable({
|
|
27
|
+
tables,
|
|
28
|
+
selected,
|
|
29
|
+
onToggleSelect,
|
|
30
|
+
onToggleSelectAll,
|
|
31
|
+
onSelect,
|
|
32
|
+
onOpen,
|
|
33
|
+
}: TableListTableProps) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="rounded-lg border">
|
|
36
|
+
<Table>
|
|
37
|
+
<TableHeader>
|
|
38
|
+
<TableRow>
|
|
39
|
+
<TableHead className="w-10">
|
|
40
|
+
<Checkbox
|
|
41
|
+
checked={
|
|
42
|
+
tables.length > 0 && selected.size === tables.length
|
|
43
|
+
}
|
|
44
|
+
onCheckedChange={onToggleSelectAll}
|
|
45
|
+
/>
|
|
46
|
+
</TableHead>
|
|
47
|
+
<TableHead>Name</TableHead>
|
|
48
|
+
<TableHead>Project</TableHead>
|
|
49
|
+
<TableHead className="text-right">Columns</TableHead>
|
|
50
|
+
<TableHead className="text-right">Rows</TableHead>
|
|
51
|
+
<TableHead>Source</TableHead>
|
|
52
|
+
<TableHead>Updated</TableHead>
|
|
53
|
+
</TableRow>
|
|
54
|
+
</TableHeader>
|
|
55
|
+
<TableBody>
|
|
56
|
+
{tables.map((t) => (
|
|
57
|
+
<TableRow
|
|
58
|
+
key={t.id}
|
|
59
|
+
className="cursor-pointer"
|
|
60
|
+
onClick={() => onSelect(t.id)}
|
|
61
|
+
onDoubleClick={() => onOpen(t.id)}
|
|
62
|
+
>
|
|
63
|
+
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
64
|
+
<Checkbox
|
|
65
|
+
checked={selected.has(t.id)}
|
|
66
|
+
onCheckedChange={() => onToggleSelect(t.id)}
|
|
67
|
+
/>
|
|
68
|
+
</TableCell>
|
|
69
|
+
<TableCell className="font-medium">{t.name}</TableCell>
|
|
70
|
+
<TableCell className="text-muted-foreground">
|
|
71
|
+
{t.projectName ?? "—"}
|
|
72
|
+
</TableCell>
|
|
73
|
+
<TableCell className="text-right text-muted-foreground">
|
|
74
|
+
{t.columnCount}
|
|
75
|
+
</TableCell>
|
|
76
|
+
<TableCell className="text-right text-muted-foreground">
|
|
77
|
+
{formatRowCount(t.rowCount)}
|
|
78
|
+
</TableCell>
|
|
79
|
+
<TableCell>
|
|
80
|
+
<Badge variant={tableSourceVariant[t.source]}>
|
|
81
|
+
{t.source}
|
|
82
|
+
</Badge>
|
|
83
|
+
</TableCell>
|
|
84
|
+
<TableCell className="text-muted-foreground">
|
|
85
|
+
{t.updatedAt
|
|
86
|
+
? new Date(t.updatedAt).toLocaleDateString()
|
|
87
|
+
: "—"}
|
|
88
|
+
</TableCell>
|
|
89
|
+
</TableRow>
|
|
90
|
+
))}
|
|
91
|
+
</TableBody>
|
|
92
|
+
</Table>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|