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.
- 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/globals.css +14 -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/book/book-reader.tsx +62 -9
- package/src/components/book/content-blocks.tsx +6 -1
- 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-row-sheet.tsx +271 -0
- package/src/components/tables/table-spreadsheet.tsx +394 -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 +11 -35
- 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/profiles/builtins/document-writer/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/book/chapter-generator.ts +81 -5
- package/src/lib/book/chapter-mapping.ts +58 -24
- package/src/lib/book/content.ts +83 -47
- package/src/lib/book/markdown-parser.ts +1 -1
- package/src/lib/book/reading-paths.ts +8 -8
- package/src/lib/book/types.ts +1 -1
- package/src/lib/book/update-detector.ts +4 -1
- 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/chat/tools/workflow-tools.ts +9 -1
- 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/src/lib/workflows/types.ts +1 -1
- package/tsconfig.json +3 -1
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Sheet,
|
|
6
6
|
SheetContent,
|
|
@@ -8,13 +8,21 @@ import {
|
|
|
8
8
|
SheetTitle,
|
|
9
9
|
SheetDescription,
|
|
10
10
|
} from "@/components/ui/sheet";
|
|
11
|
+
import {
|
|
12
|
+
Select,
|
|
13
|
+
SelectContent,
|
|
14
|
+
SelectItem,
|
|
15
|
+
SelectTrigger,
|
|
16
|
+
SelectValue,
|
|
17
|
+
} from "@/components/ui/select";
|
|
11
18
|
import { Button } from "@/components/ui/button";
|
|
12
19
|
import { Input } from "@/components/ui/input";
|
|
13
20
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
14
21
|
import { Badge } from "@/components/ui/badge";
|
|
15
22
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
16
|
-
import { FileText, Search, Loader2 } from "lucide-react";
|
|
23
|
+
import { FileText, Search, Loader2, ChevronRight, X } from "lucide-react";
|
|
17
24
|
import { getFileIcon, formatSize, getStatusDotColor } from "@/components/documents/utils";
|
|
25
|
+
import { cn } from "@/lib/utils";
|
|
18
26
|
|
|
19
27
|
export interface PickerDocument {
|
|
20
28
|
id: string;
|
|
@@ -33,6 +41,15 @@ export interface PickerDocument {
|
|
|
33
41
|
createdAt: number;
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/** Lightweight metadata for a selected document (survives project switches) */
|
|
45
|
+
export interface PickerSelectedDoc {
|
|
46
|
+
id: string;
|
|
47
|
+
originalName: string;
|
|
48
|
+
mimeType: string;
|
|
49
|
+
size: number;
|
|
50
|
+
projectName?: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
interface DocumentPickerSheetProps {
|
|
37
54
|
open: boolean;
|
|
38
55
|
onOpenChange: (open: boolean) => void;
|
|
@@ -40,14 +57,18 @@ interface DocumentPickerSheetProps {
|
|
|
40
57
|
projectId: string | null;
|
|
41
58
|
/** Currently selected document IDs (to pre-check) */
|
|
42
59
|
selectedIds: Set<string>;
|
|
43
|
-
/** Called when user confirms selection */
|
|
44
|
-
onConfirm: (selectedIds: string[]) => void;
|
|
60
|
+
/** Called when user confirms selection. Second arg provides metadata for display (avoids re-fetch). */
|
|
61
|
+
onConfirm: (selectedIds: string[], selectedMeta: PickerSelectedDoc[]) => void;
|
|
45
62
|
/** Optional: scope to a step ID label */
|
|
46
63
|
stepLabel?: string;
|
|
47
64
|
/** Grouping mode: "workflow" groups by source workflow, "project" by project name, "source" by direction */
|
|
48
65
|
groupBy?: "workflow" | "project" | "source";
|
|
49
66
|
/** Override the sheet title */
|
|
50
67
|
title?: string;
|
|
68
|
+
/** Enable cross-project browsing with a project dropdown. Default: false */
|
|
69
|
+
allowCrossProject?: boolean;
|
|
70
|
+
/** Metadata for pre-selected docs (enables tray display across project switches) */
|
|
71
|
+
selectedDocumentMeta?: Array<{ id: string; originalName: string; mimeType: string; size?: number; projectName?: string | null }>;
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
export function DocumentPickerSheet({
|
|
@@ -59,25 +80,78 @@ export function DocumentPickerSheet({
|
|
|
59
80
|
stepLabel,
|
|
60
81
|
groupBy = "source",
|
|
61
82
|
title,
|
|
83
|
+
allowCrossProject = false,
|
|
84
|
+
selectedDocumentMeta,
|
|
62
85
|
}: DocumentPickerSheetProps) {
|
|
63
86
|
const [documents, setDocuments] = useState<PickerDocument[]>([]);
|
|
64
87
|
const [loading, setLoading] = useState(false);
|
|
65
88
|
const [search, setSearch] = useState("");
|
|
66
89
|
const [localSelected, setLocalSelected] = useState<Set<string>>(new Set());
|
|
67
90
|
|
|
68
|
-
//
|
|
91
|
+
// Cross-project state
|
|
92
|
+
const [activeProjectId, setActiveProjectId] = useState<string | null>(projectId);
|
|
93
|
+
const [projects, setProjects] = useState<Array<{ id: string; name: string }>>([]);
|
|
94
|
+
const [projectsLoading, setProjectsLoading] = useState(false);
|
|
95
|
+
const [selectedDocMeta, setSelectedDocMeta] = useState<Map<string, PickerSelectedDoc>>(new Map());
|
|
96
|
+
const [trayExpanded, setTrayExpanded] = useState(true);
|
|
97
|
+
const initializedRef = useRef(false);
|
|
98
|
+
|
|
99
|
+
// Initialize state when sheet opens
|
|
69
100
|
useEffect(() => {
|
|
70
101
|
if (open) {
|
|
102
|
+
setActiveProjectId(projectId);
|
|
71
103
|
setLocalSelected(new Set(selectedIds));
|
|
72
|
-
|
|
104
|
+
setSearch("");
|
|
105
|
+
setTrayExpanded(true);
|
|
106
|
+
|
|
107
|
+
// Seed selectedDocMeta from the prop
|
|
108
|
+
if (selectedDocumentMeta && selectedDocumentMeta.length > 0) {
|
|
109
|
+
const metaMap = new Map<string, PickerSelectedDoc>();
|
|
110
|
+
for (const doc of selectedDocumentMeta) {
|
|
111
|
+
if (selectedIds.has(doc.id)) {
|
|
112
|
+
metaMap.set(doc.id, {
|
|
113
|
+
id: doc.id,
|
|
114
|
+
originalName: doc.originalName,
|
|
115
|
+
mimeType: doc.mimeType,
|
|
116
|
+
size: doc.size ?? 0,
|
|
117
|
+
projectName: doc.projectName,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
setSelectedDocMeta(metaMap);
|
|
122
|
+
} else {
|
|
123
|
+
setSelectedDocMeta(new Map());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fetch projects list for the dropdown
|
|
127
|
+
if (allowCrossProject) {
|
|
128
|
+
fetchProjects();
|
|
129
|
+
}
|
|
130
|
+
initializedRef.current = true;
|
|
131
|
+
} else {
|
|
132
|
+
initializedRef.current = false;
|
|
133
|
+
}
|
|
134
|
+
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
135
|
+
|
|
136
|
+
// Fetch documents when activeProjectId changes (and sheet is open)
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (open && initializedRef.current) {
|
|
139
|
+
fetchDocuments(activeProjectId);
|
|
140
|
+
}
|
|
141
|
+
}, [activeProjectId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
142
|
+
|
|
143
|
+
// Initial document fetch when sheet opens
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (open) {
|
|
146
|
+
fetchDocuments(projectId);
|
|
73
147
|
}
|
|
74
148
|
}, [open, projectId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
75
149
|
|
|
76
|
-
async function fetchDocuments() {
|
|
150
|
+
async function fetchDocuments(forProjectId: string | null) {
|
|
77
151
|
setLoading(true);
|
|
78
152
|
try {
|
|
79
153
|
const params = new URLSearchParams({ status: "ready" });
|
|
80
|
-
if (
|
|
154
|
+
if (forProjectId) params.set("projectId", forProjectId);
|
|
81
155
|
const res = await fetch(`/api/documents?${params}`);
|
|
82
156
|
if (res.ok) {
|
|
83
157
|
const data = await res.json();
|
|
@@ -90,6 +164,21 @@ export function DocumentPickerSheet({
|
|
|
90
164
|
}
|
|
91
165
|
}
|
|
92
166
|
|
|
167
|
+
async function fetchProjects() {
|
|
168
|
+
setProjectsLoading(true);
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch("/api/projects");
|
|
171
|
+
if (res.ok) {
|
|
172
|
+
const data = await res.json();
|
|
173
|
+
setProjects(data.map((p: { id: string; name: string }) => ({ id: p.id, name: p.name })));
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Silently fail — no project dropdown options
|
|
177
|
+
} finally {
|
|
178
|
+
setProjectsLoading(false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
93
182
|
const filtered = useMemo(() => {
|
|
94
183
|
if (!search.trim()) return documents;
|
|
95
184
|
const q = search.toLowerCase();
|
|
@@ -136,32 +225,71 @@ export function DocumentPickerSheet({
|
|
|
136
225
|
return groups;
|
|
137
226
|
}, [filtered, groupBy]);
|
|
138
227
|
|
|
228
|
+
// Compute cross-project tray items
|
|
229
|
+
const currentDocIds = useMemo(() => new Set(documents.map((d) => d.id)), [documents]);
|
|
230
|
+
const crossProjectSelected = useMemo(
|
|
231
|
+
() => [...selectedDocMeta.values()].filter((d) => localSelected.has(d.id) && !currentDocIds.has(d.id)),
|
|
232
|
+
[selectedDocMeta, localSelected, currentDocIds]
|
|
233
|
+
);
|
|
234
|
+
const showTray = allowCrossProject && crossProjectSelected.length > 0;
|
|
235
|
+
|
|
139
236
|
function toggleDocument(id: string) {
|
|
140
237
|
setLocalSelected((prev) => {
|
|
141
238
|
const next = new Set(prev);
|
|
142
239
|
if (next.has(id)) {
|
|
143
240
|
next.delete(id);
|
|
241
|
+
setSelectedDocMeta((m) => {
|
|
242
|
+
const n = new Map(m);
|
|
243
|
+
n.delete(id);
|
|
244
|
+
return n;
|
|
245
|
+
});
|
|
144
246
|
} else {
|
|
145
247
|
next.add(id);
|
|
248
|
+
const doc = documents.find((d) => d.id === id);
|
|
249
|
+
if (doc) {
|
|
250
|
+
setSelectedDocMeta((m) =>
|
|
251
|
+
new Map(m).set(id, {
|
|
252
|
+
id: doc.id,
|
|
253
|
+
originalName: doc.originalName,
|
|
254
|
+
mimeType: doc.mimeType,
|
|
255
|
+
size: doc.size,
|
|
256
|
+
projectName: doc.projectName,
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
}
|
|
146
260
|
}
|
|
147
261
|
return next;
|
|
148
262
|
});
|
|
149
263
|
}
|
|
150
264
|
|
|
151
265
|
function handleConfirm() {
|
|
152
|
-
|
|
266
|
+
const ids = [...localSelected];
|
|
267
|
+
// Build metadata array from cached meta + current documents
|
|
268
|
+
const meta: PickerSelectedDoc[] = ids.map((id) => {
|
|
269
|
+
const cached = selectedDocMeta.get(id);
|
|
270
|
+
if (cached) return cached;
|
|
271
|
+
const doc = documents.find((d) => d.id === id);
|
|
272
|
+
if (doc) return { id: doc.id, originalName: doc.originalName, mimeType: doc.mimeType, size: doc.size, projectName: doc.projectName };
|
|
273
|
+
return { id, originalName: "Unknown", mimeType: "application/octet-stream", size: 0 };
|
|
274
|
+
});
|
|
275
|
+
onConfirm(ids, meta);
|
|
153
276
|
onOpenChange(false);
|
|
154
277
|
}
|
|
155
278
|
|
|
279
|
+
function handleProjectChange(value: string) {
|
|
280
|
+
setActiveProjectId(value === "__all__" ? null : value);
|
|
281
|
+
}
|
|
282
|
+
|
|
156
283
|
const sheetTitle = title
|
|
157
284
|
? title
|
|
158
285
|
: stepLabel
|
|
159
286
|
? `Select Documents for "${stepLabel}"`
|
|
160
287
|
: "Select Input Documents";
|
|
161
288
|
|
|
289
|
+
const effectiveProjectId = allowCrossProject ? activeProjectId : projectId;
|
|
162
290
|
const emptyMessage = search
|
|
163
291
|
? "No documents match your search."
|
|
164
|
-
:
|
|
292
|
+
: effectiveProjectId
|
|
165
293
|
? "No documents available in this project."
|
|
166
294
|
: "No documents available. Upload files in the Documents view.";
|
|
167
295
|
|
|
@@ -179,6 +307,28 @@ export function DocumentPickerSheet({
|
|
|
179
307
|
</SheetHeader>
|
|
180
308
|
|
|
181
309
|
<div className="px-6 pb-6 flex flex-col gap-4 flex-1 min-h-0">
|
|
310
|
+
{/* Project selector (cross-project mode only) */}
|
|
311
|
+
{allowCrossProject && (
|
|
312
|
+
<Select
|
|
313
|
+
value={activeProjectId ?? "__all__"}
|
|
314
|
+
onValueChange={handleProjectChange}
|
|
315
|
+
disabled={projectsLoading}
|
|
316
|
+
>
|
|
317
|
+
<SelectTrigger className="h-9">
|
|
318
|
+
<SelectValue placeholder="All Projects" />
|
|
319
|
+
</SelectTrigger>
|
|
320
|
+
<SelectContent>
|
|
321
|
+
<SelectItem value="__all__">All Projects</SelectItem>
|
|
322
|
+
{projects.map((p) => (
|
|
323
|
+
<SelectItem key={p.id} value={p.id}>
|
|
324
|
+
{p.name}
|
|
325
|
+
{p.id === projectId ? " (current)" : ""}
|
|
326
|
+
</SelectItem>
|
|
327
|
+
))}
|
|
328
|
+
</SelectContent>
|
|
329
|
+
</Select>
|
|
330
|
+
)}
|
|
331
|
+
|
|
182
332
|
{/* Search */}
|
|
183
333
|
<div className="relative">
|
|
184
334
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
@@ -187,9 +337,62 @@ export function DocumentPickerSheet({
|
|
|
187
337
|
value={search}
|
|
188
338
|
onChange={(e) => setSearch(e.target.value)}
|
|
189
339
|
className="pl-9"
|
|
190
|
-
|
|
340
|
+
/>
|
|
191
341
|
</div>
|
|
192
342
|
|
|
343
|
+
{/* Cross-project selected tray */}
|
|
344
|
+
{showTray && (
|
|
345
|
+
<div className="border rounded-lg p-2 space-y-1">
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
onClick={() => setTrayExpanded((v) => !v)}
|
|
349
|
+
className="w-full text-xs text-muted-foreground flex items-center gap-1 hover:text-foreground transition-colors"
|
|
350
|
+
>
|
|
351
|
+
<ChevronRight
|
|
352
|
+
className={cn(
|
|
353
|
+
"h-3 w-3 transition-transform",
|
|
354
|
+
trayExpanded && "rotate-90"
|
|
355
|
+
)}
|
|
356
|
+
/>
|
|
357
|
+
{crossProjectSelected.length} selected from other projects
|
|
358
|
+
</button>
|
|
359
|
+
{trayExpanded && (
|
|
360
|
+
<div className="max-h-[120px] overflow-y-auto space-y-1">
|
|
361
|
+
{crossProjectSelected.map((doc) => {
|
|
362
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
key={doc.id}
|
|
366
|
+
className="flex items-center gap-2 text-xs py-1.5 px-1"
|
|
367
|
+
>
|
|
368
|
+
<Icon className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
|
369
|
+
<span className="truncate flex-1">
|
|
370
|
+
{doc.originalName}
|
|
371
|
+
</span>
|
|
372
|
+
{doc.projectName && (
|
|
373
|
+
<Badge
|
|
374
|
+
variant="outline"
|
|
375
|
+
className="text-[10px] px-1.5 flex-shrink-0"
|
|
376
|
+
>
|
|
377
|
+
{doc.projectName}
|
|
378
|
+
</Badge>
|
|
379
|
+
)}
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
onClick={() => toggleDocument(doc.id)}
|
|
383
|
+
className="flex-shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
384
|
+
aria-label={`Remove ${doc.originalName}`}
|
|
385
|
+
>
|
|
386
|
+
<X className="h-3 w-3" />
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
})}
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
|
|
193
396
|
{/* Document list */}
|
|
194
397
|
<ScrollArea className="flex-1 min-h-0 -mx-2">
|
|
195
398
|
{loading ? (
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "@/components/ui/select";
|
|
14
|
+
import { LayoutGrid, LayoutList, Plus, Trash2, Search, LayoutTemplate } from "lucide-react";
|
|
15
|
+
import { toast } from "sonner";
|
|
16
|
+
import { TableListTable } from "./table-list-table";
|
|
17
|
+
import { TableGrid } from "./table-grid";
|
|
18
|
+
import { TableCreateSheet } from "./table-create-sheet";
|
|
19
|
+
import { FilterBar } from "@/components/shared/filter-bar";
|
|
20
|
+
import { EmptyState } from "@/components/shared/empty-state";
|
|
21
|
+
import { Table2 } from "lucide-react";
|
|
22
|
+
import type { TableWithRelations } from "./types";
|
|
23
|
+
|
|
24
|
+
interface TableBrowserProps {
|
|
25
|
+
initialTables: TableWithRelations[];
|
|
26
|
+
projects: { id: string; name: string }[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function TableBrowser({ initialTables, projects }: TableBrowserProps) {
|
|
30
|
+
const [tables, setTables] = useState(initialTables);
|
|
31
|
+
const [view, setView] = useState<"table" | "grid">("table");
|
|
32
|
+
const [search, setSearch] = useState("");
|
|
33
|
+
const [sourceFilter, setSourceFilter] = useState<string>("all");
|
|
34
|
+
const [projectFilter, setProjectFilter] = useState<string>("all");
|
|
35
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
36
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
37
|
+
const [deleting, setDeleting] = useState(false);
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
|
|
40
|
+
const refresh = useCallback(async () => {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch("/api/tables");
|
|
43
|
+
if (res.ok) {
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
setTables(data);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Silent refresh failure
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const filtered = tables.filter((t) => {
|
|
53
|
+
if (
|
|
54
|
+
search &&
|
|
55
|
+
!t.name.toLowerCase().includes(search.toLowerCase()) &&
|
|
56
|
+
!(t.description ?? "").toLowerCase().includes(search.toLowerCase())
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (sourceFilter !== "all" && t.source !== sourceFilter) return false;
|
|
61
|
+
if (projectFilter !== "all" && t.projectId !== projectFilter) return false;
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function toggleSelect(id: string) {
|
|
66
|
+
setSelected((prev) => {
|
|
67
|
+
const next = new Set(prev);
|
|
68
|
+
if (next.has(id)) next.delete(id);
|
|
69
|
+
else next.add(id);
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toggleSelectAll() {
|
|
75
|
+
if (selected.size === filtered.length) {
|
|
76
|
+
setSelected(new Set());
|
|
77
|
+
} else {
|
|
78
|
+
setSelected(new Set(filtered.map((t) => t.id)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleBulkDelete() {
|
|
83
|
+
if (selected.size === 0) return;
|
|
84
|
+
setDeleting(true);
|
|
85
|
+
try {
|
|
86
|
+
const promises = Array.from(selected).map((id) =>
|
|
87
|
+
fetch(`/api/tables/${id}`, { method: "DELETE" })
|
|
88
|
+
);
|
|
89
|
+
await Promise.all(promises);
|
|
90
|
+
toast.success(`Deleted ${selected.size} table(s)`);
|
|
91
|
+
setSelected(new Set());
|
|
92
|
+
await refresh();
|
|
93
|
+
} catch {
|
|
94
|
+
toast.error("Failed to delete tables");
|
|
95
|
+
} finally {
|
|
96
|
+
setDeleting(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const activeFilters = [
|
|
101
|
+
sourceFilter !== "all",
|
|
102
|
+
projectFilter !== "all",
|
|
103
|
+
].filter(Boolean).length;
|
|
104
|
+
|
|
105
|
+
const navigate = (id: string) => router.push(`/tables/${id}`);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="space-y-4">
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<div className="relative flex-1">
|
|
111
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
112
|
+
<Input
|
|
113
|
+
placeholder="Search tables..."
|
|
114
|
+
value={search}
|
|
115
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
116
|
+
className="pl-9"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex items-center gap-1 border rounded-lg p-0.5">
|
|
120
|
+
<Button
|
|
121
|
+
variant={view === "table" ? "secondary" : "ghost"}
|
|
122
|
+
size="icon"
|
|
123
|
+
className="h-8 w-8"
|
|
124
|
+
onClick={() => setView("table")}
|
|
125
|
+
>
|
|
126
|
+
<LayoutList className="h-4 w-4" />
|
|
127
|
+
</Button>
|
|
128
|
+
<Button
|
|
129
|
+
variant={view === "grid" ? "secondary" : "ghost"}
|
|
130
|
+
size="icon"
|
|
131
|
+
className="h-8 w-8"
|
|
132
|
+
onClick={() => setView("grid")}
|
|
133
|
+
>
|
|
134
|
+
<LayoutGrid className="h-4 w-4" />
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
{selected.size > 0 && (
|
|
138
|
+
<Button
|
|
139
|
+
variant="destructive"
|
|
140
|
+
size="sm"
|
|
141
|
+
disabled={deleting}
|
|
142
|
+
onClick={handleBulkDelete}
|
|
143
|
+
>
|
|
144
|
+
<Trash2 className="h-4 w-4 mr-1" />
|
|
145
|
+
Delete ({selected.size})
|
|
146
|
+
</Button>
|
|
147
|
+
)}
|
|
148
|
+
<Button
|
|
149
|
+
variant="outline"
|
|
150
|
+
size="sm"
|
|
151
|
+
onClick={() => router.push("/tables/templates")}
|
|
152
|
+
>
|
|
153
|
+
<LayoutTemplate className="h-4 w-4 mr-1" />
|
|
154
|
+
Templates
|
|
155
|
+
</Button>
|
|
156
|
+
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
|
157
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
158
|
+
New Table
|
|
159
|
+
</Button>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<FilterBar
|
|
163
|
+
activeCount={activeFilters}
|
|
164
|
+
onClear={() => {
|
|
165
|
+
setSourceFilter("all");
|
|
166
|
+
setProjectFilter("all");
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<Select value={sourceFilter} onValueChange={setSourceFilter}>
|
|
170
|
+
<SelectTrigger className="w-[140px]">
|
|
171
|
+
<SelectValue placeholder="Source" />
|
|
172
|
+
</SelectTrigger>
|
|
173
|
+
<SelectContent>
|
|
174
|
+
<SelectItem value="all">All Sources</SelectItem>
|
|
175
|
+
<SelectItem value="manual">Manual</SelectItem>
|
|
176
|
+
<SelectItem value="imported">Imported</SelectItem>
|
|
177
|
+
<SelectItem value="agent">Agent</SelectItem>
|
|
178
|
+
<SelectItem value="template">Template</SelectItem>
|
|
179
|
+
</SelectContent>
|
|
180
|
+
</Select>
|
|
181
|
+
|
|
182
|
+
<Select value={projectFilter} onValueChange={setProjectFilter}>
|
|
183
|
+
<SelectTrigger className="w-[180px]">
|
|
184
|
+
<SelectValue placeholder="Project" />
|
|
185
|
+
</SelectTrigger>
|
|
186
|
+
<SelectContent>
|
|
187
|
+
<SelectItem value="all">All Projects</SelectItem>
|
|
188
|
+
{projects.map((p) => (
|
|
189
|
+
<SelectItem key={p.id} value={p.id}>
|
|
190
|
+
{p.name}
|
|
191
|
+
</SelectItem>
|
|
192
|
+
))}
|
|
193
|
+
</SelectContent>
|
|
194
|
+
</Select>
|
|
195
|
+
</FilterBar>
|
|
196
|
+
|
|
197
|
+
{filtered.length === 0 ? (
|
|
198
|
+
<EmptyState
|
|
199
|
+
icon={Table2}
|
|
200
|
+
heading="No tables yet"
|
|
201
|
+
description="Create a table to start organizing your structured data, or browse templates for inspiration."
|
|
202
|
+
action={
|
|
203
|
+
<Button onClick={() => setCreateOpen(true)}>
|
|
204
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
205
|
+
Create Table
|
|
206
|
+
</Button>
|
|
207
|
+
}
|
|
208
|
+
/>
|
|
209
|
+
) : view === "table" ? (
|
|
210
|
+
<TableListTable
|
|
211
|
+
tables={filtered}
|
|
212
|
+
selected={selected}
|
|
213
|
+
onToggleSelect={toggleSelect}
|
|
214
|
+
onToggleSelectAll={toggleSelectAll}
|
|
215
|
+
onSelect={navigate}
|
|
216
|
+
onOpen={navigate}
|
|
217
|
+
/>
|
|
218
|
+
) : (
|
|
219
|
+
<TableGrid
|
|
220
|
+
tables={filtered}
|
|
221
|
+
onSelect={navigate}
|
|
222
|
+
onOpen={navigate}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
<TableCreateSheet
|
|
227
|
+
open={createOpen}
|
|
228
|
+
onOpenChange={setCreateOpen}
|
|
229
|
+
projects={projects}
|
|
230
|
+
onCreated={refresh}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|