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,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@/components/ui/dropdown-menu";
|
|
11
|
+
import {
|
|
12
|
+
Type,
|
|
13
|
+
Hash,
|
|
14
|
+
Calendar,
|
|
15
|
+
CheckSquare,
|
|
16
|
+
List,
|
|
17
|
+
Link,
|
|
18
|
+
Mail,
|
|
19
|
+
GitBranch,
|
|
20
|
+
Sparkles,
|
|
21
|
+
ArrowUp,
|
|
22
|
+
ArrowDown,
|
|
23
|
+
ChevronDown,
|
|
24
|
+
Trash2,
|
|
25
|
+
Pencil,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import type { ColumnDef } from "@/lib/tables/types";
|
|
28
|
+
import type { ColumnDataType } from "@/lib/constants/table-status";
|
|
29
|
+
import type { LucideIcon } from "lucide-react";
|
|
30
|
+
|
|
31
|
+
const typeIcons: Record<ColumnDataType, LucideIcon> = {
|
|
32
|
+
text: Type,
|
|
33
|
+
number: Hash,
|
|
34
|
+
date: Calendar,
|
|
35
|
+
boolean: CheckSquare,
|
|
36
|
+
select: List,
|
|
37
|
+
url: Link,
|
|
38
|
+
email: Mail,
|
|
39
|
+
relation: GitBranch,
|
|
40
|
+
computed: Sparkles,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface ColumnHeaderProps {
|
|
44
|
+
column: ColumnDef;
|
|
45
|
+
sortDirection: "asc" | "desc" | null;
|
|
46
|
+
onSort: (direction: "asc" | "desc") => void;
|
|
47
|
+
onRename: () => void;
|
|
48
|
+
onDelete: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function SpreadsheetColumnHeader({
|
|
52
|
+
column,
|
|
53
|
+
sortDirection,
|
|
54
|
+
onSort,
|
|
55
|
+
onRename,
|
|
56
|
+
onDelete,
|
|
57
|
+
}: ColumnHeaderProps) {
|
|
58
|
+
const Icon = typeIcons[column.dataType] ?? Type;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex items-center gap-1 w-full">
|
|
62
|
+
<Icon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
63
|
+
<span className="truncate text-xs font-medium">{column.displayName}</span>
|
|
64
|
+
{sortDirection === "asc" && <ArrowUp className="h-3 w-3 text-muted-foreground" />}
|
|
65
|
+
{sortDirection === "desc" && <ArrowDown className="h-3 w-3 text-muted-foreground" />}
|
|
66
|
+
|
|
67
|
+
<DropdownMenu>
|
|
68
|
+
<DropdownMenuTrigger asChild>
|
|
69
|
+
<Button
|
|
70
|
+
variant="ghost"
|
|
71
|
+
size="icon"
|
|
72
|
+
className="h-5 w-5 ml-auto shrink-0 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
|
73
|
+
>
|
|
74
|
+
<ChevronDown className="h-3 w-3" />
|
|
75
|
+
</Button>
|
|
76
|
+
</DropdownMenuTrigger>
|
|
77
|
+
<DropdownMenuContent align="start">
|
|
78
|
+
<DropdownMenuItem onClick={() => onSort("asc")}>
|
|
79
|
+
<ArrowUp className="h-4 w-4 mr-2" />
|
|
80
|
+
Sort Ascending
|
|
81
|
+
</DropdownMenuItem>
|
|
82
|
+
<DropdownMenuItem onClick={() => onSort("desc")}>
|
|
83
|
+
<ArrowDown className="h-4 w-4 mr-2" />
|
|
84
|
+
Sort Descending
|
|
85
|
+
</DropdownMenuItem>
|
|
86
|
+
<DropdownMenuSeparator />
|
|
87
|
+
<DropdownMenuItem onClick={onRename}>
|
|
88
|
+
<Pencil className="h-4 w-4 mr-2" />
|
|
89
|
+
Rename
|
|
90
|
+
</DropdownMenuItem>
|
|
91
|
+
<DropdownMenuSeparator />
|
|
92
|
+
<DropdownMenuItem
|
|
93
|
+
onClick={onDelete}
|
|
94
|
+
className="text-destructive focus:text-destructive"
|
|
95
|
+
>
|
|
96
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
97
|
+
Delete Column
|
|
98
|
+
</DropdownMenuItem>
|
|
99
|
+
</DropdownMenuContent>
|
|
100
|
+
</DropdownMenu>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { Label } from "@/components/ui/label";
|
|
7
|
+
import { Switch } from "@/components/ui/switch";
|
|
8
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
9
|
+
import {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from "@/components/ui/select";
|
|
16
|
+
import {
|
|
17
|
+
Sheet,
|
|
18
|
+
SheetContent,
|
|
19
|
+
SheetHeader,
|
|
20
|
+
SheetTitle,
|
|
21
|
+
SheetFooter,
|
|
22
|
+
} from "@/components/ui/sheet";
|
|
23
|
+
import { Plus, X } from "lucide-react";
|
|
24
|
+
import { toast } from "sonner";
|
|
25
|
+
import { COLUMN_DATA_TYPES, columnTypeLabel } from "@/lib/constants/table-status";
|
|
26
|
+
import type { ColumnDataType } from "@/lib/constants/table-status";
|
|
27
|
+
|
|
28
|
+
interface TableColumnSheetProps {
|
|
29
|
+
tableId: string;
|
|
30
|
+
open: boolean;
|
|
31
|
+
onOpenChange: (open: boolean) => void;
|
|
32
|
+
onColumnAdded: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface TableInfo {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
columnSchema: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function TableColumnSheet({
|
|
42
|
+
tableId,
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
onColumnAdded,
|
|
46
|
+
}: TableColumnSheetProps) {
|
|
47
|
+
const [displayName, setDisplayName] = useState("");
|
|
48
|
+
const [dataType, setDataType] = useState<ColumnDataType>("text");
|
|
49
|
+
const [required, setRequired] = useState(false);
|
|
50
|
+
const [selectOptions, setSelectOptions] = useState<string[]>([""]);
|
|
51
|
+
const [saving, setSaving] = useState(false);
|
|
52
|
+
|
|
53
|
+
// Computed column state
|
|
54
|
+
const [formula, setFormula] = useState("");
|
|
55
|
+
const [formulaType, setFormulaType] = useState<string>("arithmetic");
|
|
56
|
+
|
|
57
|
+
// Relation column state
|
|
58
|
+
const [targetTableId, setTargetTableId] = useState("");
|
|
59
|
+
const [displayColumn, setDisplayColumn] = useState("");
|
|
60
|
+
const [availableTables, setAvailableTables] = useState<TableInfo[]>([]);
|
|
61
|
+
const [targetColumns, setTargetColumns] = useState<string[]>([]);
|
|
62
|
+
|
|
63
|
+
// Fetch available tables when relation type is selected
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (dataType !== "relation" || !open) return;
|
|
66
|
+
fetch("/api/tables")
|
|
67
|
+
.then((r) => r.json())
|
|
68
|
+
.then((tables: TableInfo[]) => setAvailableTables(tables.filter((t) => t.id !== tableId)))
|
|
69
|
+
.catch(() => {});
|
|
70
|
+
}, [dataType, open, tableId]);
|
|
71
|
+
|
|
72
|
+
// Fetch target table columns when target table changes
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!targetTableId) { setTargetColumns([]); return; }
|
|
75
|
+
fetch(`/api/tables/${targetTableId}`)
|
|
76
|
+
.then((r) => r.json())
|
|
77
|
+
.then((t: TableInfo) => {
|
|
78
|
+
const cols = JSON.parse(t.columnSchema) as Array<{ name: string }>;
|
|
79
|
+
setTargetColumns(cols.map((c) => c.name));
|
|
80
|
+
if (cols.length > 0 && !displayColumn) setDisplayColumn(cols[0].name);
|
|
81
|
+
})
|
|
82
|
+
.catch(() => {});
|
|
83
|
+
}, [targetTableId]);
|
|
84
|
+
|
|
85
|
+
const name = displayName
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
88
|
+
.replace(/^_|_$/g, "");
|
|
89
|
+
|
|
90
|
+
function addOption() {
|
|
91
|
+
setSelectOptions((prev) => [...prev, ""]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateOption(index: number, value: string) {
|
|
95
|
+
setSelectOptions((prev) => prev.map((o, i) => (i === index ? value : o)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removeOption(index: number) {
|
|
99
|
+
setSelectOptions((prev) => prev.filter((_, i) => i !== index));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handleSubmit() {
|
|
103
|
+
if (!displayName.trim()) {
|
|
104
|
+
toast.error("Column name is required");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (dataType === "computed" && !formula.trim()) {
|
|
108
|
+
toast.error("Formula is required for computed columns");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (dataType === "relation" && !targetTableId) {
|
|
112
|
+
toast.error("Target table is required for relation columns");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setSaving(true);
|
|
117
|
+
try {
|
|
118
|
+
let config: Record<string, unknown> | undefined;
|
|
119
|
+
if (dataType === "select") {
|
|
120
|
+
config = { options: selectOptions.filter((o) => o.trim()) };
|
|
121
|
+
} else if (dataType === "computed") {
|
|
122
|
+
config = { formula: formula.trim(), formulaType, resultType: "text" };
|
|
123
|
+
} else if (dataType === "relation") {
|
|
124
|
+
config = { targetTableId, displayColumn: displayColumn || undefined };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const res = await fetch(`/api/tables/${tableId}/columns`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
name,
|
|
132
|
+
displayName: displayName.trim(),
|
|
133
|
+
dataType,
|
|
134
|
+
required,
|
|
135
|
+
config,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const err = await res.json();
|
|
141
|
+
toast.error(err.error?.formErrors?.[0] || "Failed to add column");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
toast.success(`Column "${displayName}" added`);
|
|
146
|
+
onOpenChange(false);
|
|
147
|
+
onColumnAdded();
|
|
148
|
+
|
|
149
|
+
// Reset
|
|
150
|
+
setDisplayName("");
|
|
151
|
+
setDataType("text");
|
|
152
|
+
setRequired(false);
|
|
153
|
+
setSelectOptions([""]);
|
|
154
|
+
setFormula("");
|
|
155
|
+
setFormulaType("arithmetic");
|
|
156
|
+
setTargetTableId("");
|
|
157
|
+
setDisplayColumn("");
|
|
158
|
+
} catch {
|
|
159
|
+
toast.error("Failed to add column");
|
|
160
|
+
} finally {
|
|
161
|
+
setSaving(false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
167
|
+
<SheetContent side="right" className="w-[380px] sm:max-w-[380px]">
|
|
168
|
+
<SheetHeader>
|
|
169
|
+
<SheetTitle>Add Column</SheetTitle>
|
|
170
|
+
</SheetHeader>
|
|
171
|
+
|
|
172
|
+
<div className="px-6 pb-6 space-y-4 overflow-y-auto">
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
<Label htmlFor="col-name">Display Name</Label>
|
|
175
|
+
<Input
|
|
176
|
+
id="col-name"
|
|
177
|
+
placeholder="e.g. Email Address"
|
|
178
|
+
value={displayName}
|
|
179
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
180
|
+
/>
|
|
181
|
+
{name && (
|
|
182
|
+
<p className="text-xs text-muted-foreground">
|
|
183
|
+
Field name: <code>{name}</code>
|
|
184
|
+
</p>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="space-y-2">
|
|
189
|
+
<Label>Type</Label>
|
|
190
|
+
<Select value={dataType} onValueChange={(v) => setDataType(v as ColumnDataType)}>
|
|
191
|
+
<SelectTrigger>
|
|
192
|
+
<SelectValue />
|
|
193
|
+
</SelectTrigger>
|
|
194
|
+
<SelectContent>
|
|
195
|
+
{COLUMN_DATA_TYPES.map((dt) => (
|
|
196
|
+
<SelectItem key={dt} value={dt}>
|
|
197
|
+
{columnTypeLabel[dt]}
|
|
198
|
+
</SelectItem>
|
|
199
|
+
))}
|
|
200
|
+
</SelectContent>
|
|
201
|
+
</Select>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="flex items-center justify-between">
|
|
205
|
+
<Label htmlFor="col-required">Required</Label>
|
|
206
|
+
<Switch
|
|
207
|
+
id="col-required"
|
|
208
|
+
checked={required}
|
|
209
|
+
onCheckedChange={setRequired}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{dataType === "select" && (
|
|
214
|
+
<div className="space-y-2">
|
|
215
|
+
<div className="flex items-center justify-between">
|
|
216
|
+
<Label>Options</Label>
|
|
217
|
+
<Button variant="ghost" size="sm" onClick={addOption}>
|
|
218
|
+
<Plus className="h-3 w-3 mr-1" />
|
|
219
|
+
Add
|
|
220
|
+
</Button>
|
|
221
|
+
</div>
|
|
222
|
+
<div className="space-y-1">
|
|
223
|
+
{selectOptions.map((opt, i) => (
|
|
224
|
+
<div key={i} className="flex items-center gap-1">
|
|
225
|
+
<Input
|
|
226
|
+
placeholder={`Option ${i + 1}`}
|
|
227
|
+
value={opt}
|
|
228
|
+
onChange={(e) => updateOption(i, e.target.value)}
|
|
229
|
+
className="h-8"
|
|
230
|
+
/>
|
|
231
|
+
<Button
|
|
232
|
+
variant="ghost"
|
|
233
|
+
size="icon"
|
|
234
|
+
className="h-8 w-8 shrink-0"
|
|
235
|
+
onClick={() => removeOption(i)}
|
|
236
|
+
disabled={selectOptions.length <= 1}
|
|
237
|
+
>
|
|
238
|
+
<X className="h-3 w-3" />
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{dataType === "computed" && (
|
|
247
|
+
<div className="space-y-3">
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
<Label>Formula</Label>
|
|
250
|
+
<Textarea
|
|
251
|
+
placeholder="e.g. {{price}} * {{quantity}}"
|
|
252
|
+
value={formula}
|
|
253
|
+
onChange={(e) => setFormula(e.target.value)}
|
|
254
|
+
rows={3}
|
|
255
|
+
className="font-mono text-sm"
|
|
256
|
+
/>
|
|
257
|
+
<p className="text-xs text-muted-foreground">
|
|
258
|
+
Reference columns with {"{{column_name}}"} syntax. Supports +, -, *, /, if(), concat(), sum(), avg(), min(), max().
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
<div className="space-y-2">
|
|
262
|
+
<Label>Formula Type</Label>
|
|
263
|
+
<Select value={formulaType} onValueChange={setFormulaType}>
|
|
264
|
+
<SelectTrigger>
|
|
265
|
+
<SelectValue />
|
|
266
|
+
</SelectTrigger>
|
|
267
|
+
<SelectContent>
|
|
268
|
+
<SelectItem value="arithmetic">Arithmetic</SelectItem>
|
|
269
|
+
<SelectItem value="text_concat">Text Concatenation</SelectItem>
|
|
270
|
+
<SelectItem value="date_diff">Date Difference</SelectItem>
|
|
271
|
+
<SelectItem value="conditional">Conditional</SelectItem>
|
|
272
|
+
<SelectItem value="aggregate">Aggregate</SelectItem>
|
|
273
|
+
</SelectContent>
|
|
274
|
+
</Select>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{dataType === "relation" && (
|
|
280
|
+
<div className="space-y-3">
|
|
281
|
+
<div className="space-y-2">
|
|
282
|
+
<Label>Target Table</Label>
|
|
283
|
+
<Select value={targetTableId} onValueChange={setTargetTableId}>
|
|
284
|
+
<SelectTrigger>
|
|
285
|
+
<SelectValue placeholder="Select a table..." />
|
|
286
|
+
</SelectTrigger>
|
|
287
|
+
<SelectContent>
|
|
288
|
+
{availableTables.map((t) => (
|
|
289
|
+
<SelectItem key={t.id} value={t.id}>
|
|
290
|
+
{t.name}
|
|
291
|
+
</SelectItem>
|
|
292
|
+
))}
|
|
293
|
+
</SelectContent>
|
|
294
|
+
</Select>
|
|
295
|
+
</div>
|
|
296
|
+
{targetColumns.length > 0 && (
|
|
297
|
+
<div className="space-y-2">
|
|
298
|
+
<Label>Display Column</Label>
|
|
299
|
+
<Select value={displayColumn} onValueChange={setDisplayColumn}>
|
|
300
|
+
<SelectTrigger>
|
|
301
|
+
<SelectValue />
|
|
302
|
+
</SelectTrigger>
|
|
303
|
+
<SelectContent>
|
|
304
|
+
{targetColumns.map((col) => (
|
|
305
|
+
<SelectItem key={col} value={col}>
|
|
306
|
+
{col}
|
|
307
|
+
</SelectItem>
|
|
308
|
+
))}
|
|
309
|
+
</SelectContent>
|
|
310
|
+
</Select>
|
|
311
|
+
<p className="text-xs text-muted-foreground">
|
|
312
|
+
The column from the target table shown in this cell.
|
|
313
|
+
</p>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<SheetFooter className="px-6">
|
|
321
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
322
|
+
Cancel
|
|
323
|
+
</Button>
|
|
324
|
+
<Button onClick={handleSubmit} disabled={saving}>
|
|
325
|
+
{saving ? "Adding..." : "Add Column"}
|
|
326
|
+
</Button>
|
|
327
|
+
</SheetFooter>
|
|
328
|
+
</SheetContent>
|
|
329
|
+
</Sheet>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { Label } from "@/components/ui/label";
|
|
7
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@/components/ui/select";
|
|
15
|
+
import {
|
|
16
|
+
Sheet,
|
|
17
|
+
SheetContent,
|
|
18
|
+
SheetHeader,
|
|
19
|
+
SheetTitle,
|
|
20
|
+
SheetFooter,
|
|
21
|
+
} from "@/components/ui/sheet";
|
|
22
|
+
import { Plus, X } from "lucide-react";
|
|
23
|
+
import { toast } from "sonner";
|
|
24
|
+
import { COLUMN_DATA_TYPES, columnTypeLabel } from "@/lib/constants/table-status";
|
|
25
|
+
import type { ColumnDataType } from "@/lib/constants/table-status";
|
|
26
|
+
|
|
27
|
+
interface ColumnDraft {
|
|
28
|
+
name: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
dataType: ColumnDataType;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface TableCreateSheetProps {
|
|
34
|
+
open: boolean;
|
|
35
|
+
onOpenChange: (open: boolean) => void;
|
|
36
|
+
projects: { id: string; name: string }[];
|
|
37
|
+
onCreated: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function TableCreateSheet({
|
|
41
|
+
open,
|
|
42
|
+
onOpenChange,
|
|
43
|
+
projects,
|
|
44
|
+
onCreated,
|
|
45
|
+
}: TableCreateSheetProps) {
|
|
46
|
+
const [name, setName] = useState("");
|
|
47
|
+
const [description, setDescription] = useState("");
|
|
48
|
+
const [projectId, setProjectId] = useState<string>("none");
|
|
49
|
+
const [columns, setColumns] = useState<ColumnDraft[]>([
|
|
50
|
+
{ name: "name", displayName: "Name", dataType: "text" },
|
|
51
|
+
]);
|
|
52
|
+
const [saving, setSaving] = useState(false);
|
|
53
|
+
|
|
54
|
+
function addColumn() {
|
|
55
|
+
setColumns((prev) => [
|
|
56
|
+
...prev,
|
|
57
|
+
{ name: "", displayName: "", dataType: "text" },
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function removeColumn(index: number) {
|
|
62
|
+
setColumns((prev) => prev.filter((_, i) => i !== index));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function updateColumn(index: number, field: keyof ColumnDraft, value: string) {
|
|
66
|
+
setColumns((prev) =>
|
|
67
|
+
prev.map((col, i) => {
|
|
68
|
+
if (i !== index) return col;
|
|
69
|
+
const updated = { ...col, [field]: value };
|
|
70
|
+
// Auto-generate name from displayName
|
|
71
|
+
if (field === "displayName") {
|
|
72
|
+
updated.name = value
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
75
|
+
.replace(/^_|_$/g, "");
|
|
76
|
+
}
|
|
77
|
+
return updated;
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleSubmit() {
|
|
83
|
+
if (!name.trim()) {
|
|
84
|
+
toast.error("Table name is required");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setSaving(true);
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch("/api/tables", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
name: name.trim(),
|
|
95
|
+
description: description.trim() || null,
|
|
96
|
+
projectId: projectId === "none" ? null : projectId,
|
|
97
|
+
columns: columns
|
|
98
|
+
.filter((c) => c.name && c.displayName)
|
|
99
|
+
.map((c, i) => ({
|
|
100
|
+
name: c.name,
|
|
101
|
+
displayName: c.displayName,
|
|
102
|
+
dataType: c.dataType,
|
|
103
|
+
position: i,
|
|
104
|
+
})),
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const err = await res.json();
|
|
110
|
+
toast.error(err.error?.formErrors?.[0] || "Failed to create table");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
toast.success("Table created");
|
|
115
|
+
onOpenChange(false);
|
|
116
|
+
onCreated();
|
|
117
|
+
|
|
118
|
+
// Reset form
|
|
119
|
+
setName("");
|
|
120
|
+
setDescription("");
|
|
121
|
+
setProjectId("none");
|
|
122
|
+
setColumns([{ name: "name", displayName: "Name", dataType: "text" }]);
|
|
123
|
+
} catch {
|
|
124
|
+
toast.error("Failed to create table");
|
|
125
|
+
} finally {
|
|
126
|
+
setSaving(false);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
132
|
+
<SheetContent side="right" className="w-[480px] sm:max-w-[480px]">
|
|
133
|
+
<SheetHeader>
|
|
134
|
+
<SheetTitle>Create Table</SheetTitle>
|
|
135
|
+
</SheetHeader>
|
|
136
|
+
|
|
137
|
+
<div className="px-6 pb-6 space-y-4 overflow-y-auto">
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
<Label htmlFor="table-name">Name</Label>
|
|
140
|
+
<Input
|
|
141
|
+
id="table-name"
|
|
142
|
+
placeholder="e.g. Customer List"
|
|
143
|
+
value={name}
|
|
144
|
+
onChange={(e) => setName(e.target.value)}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<Label htmlFor="table-desc">Description</Label>
|
|
150
|
+
<Textarea
|
|
151
|
+
id="table-desc"
|
|
152
|
+
placeholder="What is this table for?"
|
|
153
|
+
value={description}
|
|
154
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
155
|
+
rows={2}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="space-y-2">
|
|
160
|
+
<Label>Project</Label>
|
|
161
|
+
<Select value={projectId} onValueChange={setProjectId}>
|
|
162
|
+
<SelectTrigger>
|
|
163
|
+
<SelectValue placeholder="No project" />
|
|
164
|
+
</SelectTrigger>
|
|
165
|
+
<SelectContent>
|
|
166
|
+
<SelectItem value="none">No project</SelectItem>
|
|
167
|
+
{projects.map((p) => (
|
|
168
|
+
<SelectItem key={p.id} value={p.id}>
|
|
169
|
+
{p.name}
|
|
170
|
+
</SelectItem>
|
|
171
|
+
))}
|
|
172
|
+
</SelectContent>
|
|
173
|
+
</Select>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<div className="flex items-center justify-between">
|
|
178
|
+
<Label>Columns</Label>
|
|
179
|
+
<Button variant="ghost" size="sm" onClick={addColumn}>
|
|
180
|
+
<Plus className="h-3 w-3 mr-1" />
|
|
181
|
+
Add
|
|
182
|
+
</Button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div className="space-y-2">
|
|
186
|
+
{columns.map((col, i) => (
|
|
187
|
+
<div key={i} className="flex items-center gap-2">
|
|
188
|
+
<Input
|
|
189
|
+
placeholder="Column name"
|
|
190
|
+
value={col.displayName}
|
|
191
|
+
onChange={(e) => updateColumn(i, "displayName", e.target.value)}
|
|
192
|
+
className="flex-1"
|
|
193
|
+
/>
|
|
194
|
+
<Select
|
|
195
|
+
value={col.dataType}
|
|
196
|
+
onValueChange={(v) => updateColumn(i, "dataType", v)}
|
|
197
|
+
>
|
|
198
|
+
<SelectTrigger className="w-[120px]">
|
|
199
|
+
<SelectValue />
|
|
200
|
+
</SelectTrigger>
|
|
201
|
+
<SelectContent>
|
|
202
|
+
{COLUMN_DATA_TYPES.filter(
|
|
203
|
+
(dt) => dt !== "relation" && dt !== "computed"
|
|
204
|
+
).map((dt) => (
|
|
205
|
+
<SelectItem key={dt} value={dt}>
|
|
206
|
+
{columnTypeLabel[dt]}
|
|
207
|
+
</SelectItem>
|
|
208
|
+
))}
|
|
209
|
+
</SelectContent>
|
|
210
|
+
</Select>
|
|
211
|
+
<Button
|
|
212
|
+
variant="ghost"
|
|
213
|
+
size="icon"
|
|
214
|
+
className="h-8 w-8 shrink-0"
|
|
215
|
+
onClick={() => removeColumn(i)}
|
|
216
|
+
disabled={columns.length <= 1}
|
|
217
|
+
>
|
|
218
|
+
<X className="h-3 w-3" />
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<SheetFooter className="px-6">
|
|
227
|
+
<Button
|
|
228
|
+
variant="outline"
|
|
229
|
+
onClick={() => onOpenChange(false)}
|
|
230
|
+
>
|
|
231
|
+
Cancel
|
|
232
|
+
</Button>
|
|
233
|
+
<Button onClick={handleSubmit} disabled={saving}>
|
|
234
|
+
{saving ? "Creating..." : "Create Table"}
|
|
235
|
+
</Button>
|
|
236
|
+
</SheetFooter>
|
|
237
|
+
</SheetContent>
|
|
238
|
+
</Sheet>
|
|
239
|
+
);
|
|
240
|
+
}
|