stagent 0.6.2 → 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.
Files changed (176) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +272 -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 -2
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/documents/[id]/route.ts +5 -1
  17. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  18. package/src/app/api/documents/route.ts +5 -1
  19. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  20. package/src/app/api/projects/[id]/route.ts +72 -3
  21. package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
  22. package/src/app/api/schedules/route.ts +19 -1
  23. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  24. package/src/app/api/snapshots/[id]/route.ts +44 -0
  25. package/src/app/api/snapshots/route.ts +54 -0
  26. package/src/app/api/snapshots/settings/route.ts +67 -0
  27. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  28. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  29. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  30. package/src/app/api/tables/[id]/export/route.ts +94 -0
  31. package/src/app/api/tables/[id]/history/route.ts +15 -0
  32. package/src/app/api/tables/[id]/import/route.ts +111 -0
  33. package/src/app/api/tables/[id]/route.ts +86 -0
  34. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  35. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  36. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  37. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  38. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  39. package/src/app/api/tables/route.ts +65 -0
  40. package/src/app/api/tables/templates/route.ts +92 -0
  41. package/src/app/api/tasks/[id]/route.ts +37 -2
  42. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  43. package/src/app/api/tasks/route.ts +8 -9
  44. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  45. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  46. package/src/app/api/workflows/[id]/route.ts +16 -3
  47. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  48. package/src/app/api/workflows/route.ts +13 -2
  49. package/src/app/documents/page.tsx +5 -1
  50. package/src/app/layout.tsx +0 -1
  51. package/src/app/manifest.ts +3 -3
  52. package/src/app/projects/[id]/page.tsx +62 -2
  53. package/src/app/settings/page.tsx +2 -0
  54. package/src/app/tables/[id]/page.tsx +67 -0
  55. package/src/app/tables/page.tsx +21 -0
  56. package/src/app/tables/templates/page.tsx +19 -0
  57. package/src/components/chat/chat-table-result.tsx +139 -0
  58. package/src/components/documents/document-browser.tsx +1 -1
  59. package/src/components/documents/document-chip-bar.tsx +17 -1
  60. package/src/components/documents/document-detail-view.tsx +51 -0
  61. package/src/components/documents/document-grid.tsx +5 -0
  62. package/src/components/documents/document-table.tsx +4 -0
  63. package/src/components/documents/types.ts +3 -0
  64. package/src/components/projects/project-form-sheet.tsx +109 -2
  65. package/src/components/schedules/schedule-form.tsx +91 -1
  66. package/src/components/settings/data-management-section.tsx +17 -12
  67. package/src/components/settings/database-snapshots-section.tsx +469 -0
  68. package/src/components/shared/app-sidebar.tsx +2 -0
  69. package/src/components/shared/document-picker-sheet.tsx +486 -0
  70. package/src/components/tables/table-browser.tsx +234 -0
  71. package/src/components/tables/table-cell-editor.tsx +226 -0
  72. package/src/components/tables/table-chart-builder.tsx +288 -0
  73. package/src/components/tables/table-chart-view.tsx +146 -0
  74. package/src/components/tables/table-column-header.tsx +103 -0
  75. package/src/components/tables/table-column-sheet.tsx +331 -0
  76. package/src/components/tables/table-create-sheet.tsx +240 -0
  77. package/src/components/tables/table-detail-sheet.tsx +144 -0
  78. package/src/components/tables/table-detail-tabs.tsx +278 -0
  79. package/src/components/tables/table-grid.tsx +61 -0
  80. package/src/components/tables/table-history-tab.tsx +148 -0
  81. package/src/components/tables/table-import-wizard.tsx +542 -0
  82. package/src/components/tables/table-list-table.tsx +95 -0
  83. package/src/components/tables/table-relation-combobox.tsx +217 -0
  84. package/src/components/tables/table-spreadsheet.tsx +499 -0
  85. package/src/components/tables/table-template-gallery.tsx +162 -0
  86. package/src/components/tables/table-template-preview.tsx +219 -0
  87. package/src/components/tables/table-toolbar.tsx +79 -0
  88. package/src/components/tables/table-triggers-tab.tsx +446 -0
  89. package/src/components/tables/types.ts +6 -0
  90. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  91. package/src/components/tables/utils.ts +29 -0
  92. package/src/components/tasks/task-card.tsx +8 -1
  93. package/src/components/tasks/task-create-panel.tsx +111 -14
  94. package/src/components/tasks/task-detail-view.tsx +47 -0
  95. package/src/components/tasks/task-edit-dialog.tsx +103 -2
  96. package/src/components/workflows/workflow-form-view.tsx +207 -7
  97. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  98. package/src/components/workflows/workflow-list.tsx +90 -45
  99. package/src/components/workflows/workflow-status-view.tsx +168 -23
  100. package/src/instrumentation.ts +3 -0
  101. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  102. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  103. package/src/lib/agents/claude-agent.ts +3 -1
  104. package/src/lib/agents/profiles/registry.ts +6 -3
  105. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  106. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  107. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  108. package/src/lib/book/chapter-generator.ts +4 -19
  109. package/src/lib/book/chapter-mapping.ts +17 -0
  110. package/src/lib/book/content.ts +5 -16
  111. package/src/lib/book/update-detector.ts +3 -16
  112. package/src/lib/chat/engine.ts +1 -0
  113. package/src/lib/chat/stagent-tools.ts +2 -0
  114. package/src/lib/chat/system-prompt.ts +9 -1
  115. package/src/lib/chat/tool-catalog.ts +35 -0
  116. package/src/lib/chat/tools/settings-tools.ts +109 -0
  117. package/src/lib/chat/tools/table-tools.ts +955 -0
  118. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  119. package/src/lib/constants/table-status.ts +68 -0
  120. package/src/lib/data/__tests__/clear.test.ts +1 -1
  121. package/src/lib/data/clear.ts +57 -0
  122. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  123. package/src/lib/data/seed-data/conversations.ts +350 -42
  124. package/src/lib/data/seed-data/documents.ts +564 -591
  125. package/src/lib/data/seed-data/learned-context.ts +101 -22
  126. package/src/lib/data/seed-data/notifications.ts +344 -70
  127. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  128. package/src/lib/data/seed-data/profiles.ts +144 -46
  129. package/src/lib/data/seed-data/projects.ts +50 -18
  130. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  131. package/src/lib/data/seed-data/schedules.ts +208 -41
  132. package/src/lib/data/seed-data/table-templates.ts +234 -0
  133. package/src/lib/data/seed-data/tasks.ts +614 -116
  134. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  135. package/src/lib/data/seed-data/user-tables.ts +203 -0
  136. package/src/lib/data/seed-data/views.ts +52 -7
  137. package/src/lib/data/seed-data/workflows.ts +231 -84
  138. package/src/lib/data/seed.ts +55 -14
  139. package/src/lib/data/tables.ts +417 -0
  140. package/src/lib/db/bootstrap.ts +275 -0
  141. package/src/lib/db/index.ts +9 -0
  142. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  143. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  144. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  145. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  146. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  147. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  148. package/src/lib/db/schema.ts +445 -0
  149. package/src/lib/docs/reader.ts +2 -3
  150. package/src/lib/documents/context-builder.ts +75 -2
  151. package/src/lib/documents/document-resolver.ts +119 -0
  152. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  153. package/src/lib/schedules/scheduler.ts +31 -1
  154. package/src/lib/snapshots/auto-backup.ts +132 -0
  155. package/src/lib/snapshots/retention.ts +64 -0
  156. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  157. package/src/lib/tables/computed.ts +61 -0
  158. package/src/lib/tables/context-builder.ts +139 -0
  159. package/src/lib/tables/formula-engine.ts +415 -0
  160. package/src/lib/tables/history.ts +115 -0
  161. package/src/lib/tables/import.ts +343 -0
  162. package/src/lib/tables/query-builder.ts +152 -0
  163. package/src/lib/tables/trigger-evaluator.ts +146 -0
  164. package/src/lib/tables/types.ts +141 -0
  165. package/src/lib/tables/validation.ts +119 -0
  166. package/src/lib/utils/app-root.ts +20 -0
  167. package/src/lib/utils/stagent-paths.ts +20 -0
  168. package/src/lib/validators/__tests__/task.test.ts +43 -10
  169. package/src/lib/validators/task.ts +7 -1
  170. package/src/lib/workflows/blueprints/registry.ts +3 -3
  171. package/src/lib/workflows/engine.ts +24 -8
  172. package/src/lib/workflows/types.ts +14 -0
  173. package/tsconfig.json +3 -1
  174. package/public/icon.svg +0 -13
  175. package/src/components/tasks/file-upload.tsx +0 -120
  176. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -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
+ }