idea-manager 0.2.0 → 0.3.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/next.config.ts +0 -1
- package/package.json +2 -2
- package/{src/app/icon.svg → public/favicon.svg} +2 -2
- package/src/app/api/filesystem/route.ts +49 -0
- package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
- package/src/app/api/projects/[id]/items/route.ts +51 -1
- package/src/app/api/projects/[id]/scan/route.ts +73 -0
- package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
- package/src/app/api/projects/[id]/structure/route.ts +34 -3
- package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/globals.css +465 -5
- package/src/app/layout.tsx +3 -0
- package/src/app/page.tsx +260 -88
- package/src/app/projects/[id]/page.tsx +366 -183
- package/src/cli.ts +10 -10
- package/src/components/DirectoryPicker.tsx +137 -0
- package/src/components/ScanPanel.tsx +743 -0
- package/src/components/brainstorm/Editor.tsx +20 -4
- package/src/components/brainstorm/MemoPin.tsx +91 -5
- package/src/components/dashboard/SubProjectCard.tsx +76 -0
- package/src/components/dashboard/TabBar.tsx +42 -0
- package/src/components/task/ProjectTree.tsx +223 -0
- package/src/components/task/PromptEditor.tsx +107 -0
- package/src/components/task/StatusFlow.tsx +43 -0
- package/src/components/task/TaskChat.tsx +134 -0
- package/src/components/task/TaskDetail.tsx +205 -0
- package/src/components/task/TaskList.tsx +119 -0
- package/src/components/tree/CardView.tsx +206 -0
- package/src/components/tree/RefinePopover.tsx +157 -0
- package/src/components/tree/TreeNode.tsx +147 -38
- package/src/components/tree/TreeView.tsx +270 -26
- package/src/components/ui/ConfirmDialog.tsx +88 -0
- package/src/lib/ai/chat-responder.ts +4 -2
- package/src/lib/ai/cleanup.ts +87 -0
- package/src/lib/ai/client.ts +175 -58
- package/src/lib/ai/prompter.ts +19 -24
- package/src/lib/ai/refiner.ts +128 -0
- package/src/lib/ai/structurer.ts +340 -11
- package/src/lib/db/queries/context.ts +76 -0
- package/src/lib/db/queries/items.ts +133 -12
- package/src/lib/db/queries/projects.ts +12 -8
- package/src/lib/db/queries/sub-projects.ts +122 -0
- package/src/lib/db/queries/task-conversations.ts +27 -0
- package/src/lib/db/queries/task-prompts.ts +32 -0
- package/src/lib/db/queries/tasks.ts +133 -0
- package/src/lib/db/schema.ts +75 -0
- package/src/lib/mcp/server.ts +38 -39
- package/src/lib/mcp/tools.ts +47 -45
- package/src/lib/scanner.ts +573 -0
- package/src/lib/task-store.ts +97 -0
- package/src/types/index.ts +65 -0
package/src/app/page.tsx
CHANGED
|
@@ -2,33 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
+
import DirectoryPicker from '@/components/DirectoryPicker';
|
|
6
|
+
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
7
|
+
import TabBar, { type DashboardTab } from '@/components/dashboard/TabBar';
|
|
8
|
+
import SubProjectCard from '@/components/dashboard/SubProjectCard';
|
|
9
|
+
import type { ISubProjectWithStats, ITask } from '@/types';
|
|
5
10
|
|
|
6
11
|
interface IProject {
|
|
7
12
|
id: string;
|
|
8
13
|
name: string;
|
|
9
14
|
description: string;
|
|
15
|
+
project_path: string | null;
|
|
10
16
|
created_at: string;
|
|
11
17
|
updated_at: string;
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
interface ProjectWithSubs extends IProject {
|
|
21
|
+
subProjects: ISubProjectWithStats[];
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export default function Dashboard() {
|
|
15
25
|
const router = useRouter();
|
|
16
|
-
const [projects, setProjects] = useState<
|
|
26
|
+
const [projects, setProjects] = useState<ProjectWithSubs[]>([]);
|
|
27
|
+
const [todayTasks, setTodayTasks] = useState<(ITask & { projectName: string; subProjectName: string })[]>([]);
|
|
17
28
|
const [showForm, setShowForm] = useState(false);
|
|
18
29
|
const [name, setName] = useState('');
|
|
19
30
|
const [description, setDescription] = useState('');
|
|
31
|
+
const [projectPath, setProjectPath] = useState('');
|
|
20
32
|
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [showDirPicker, setShowDirPicker] = useState(false);
|
|
34
|
+
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
|
35
|
+
const [tab, setTab] = useState<DashboardTab>(() => {
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
return (localStorage.getItem('im-dashboard-tab') as DashboardTab) || 'active';
|
|
38
|
+
}
|
|
39
|
+
return 'active';
|
|
40
|
+
});
|
|
21
41
|
|
|
22
|
-
const
|
|
42
|
+
const fetchData = useCallback(async () => {
|
|
23
43
|
const res = await fetch('/api/projects');
|
|
24
|
-
const
|
|
25
|
-
|
|
44
|
+
const projectList: IProject[] = await res.json();
|
|
45
|
+
|
|
46
|
+
const withSubs = await Promise.all(
|
|
47
|
+
projectList.map(async (p) => {
|
|
48
|
+
const subRes = await fetch(`/api/projects/${p.id}/sub-projects`);
|
|
49
|
+
const subProjects: ISubProjectWithStats[] = await subRes.json();
|
|
50
|
+
return { ...p, subProjects };
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
setProjects(withSubs);
|
|
55
|
+
|
|
56
|
+
// Gather today tasks
|
|
57
|
+
const allToday: (ITask & { projectName: string; subProjectName: string })[] = [];
|
|
58
|
+
for (const p of withSubs) {
|
|
59
|
+
for (const sp of p.subProjects) {
|
|
60
|
+
for (const pt of sp.preview_tasks) {
|
|
61
|
+
// preview_tasks doesn't have full task data, so we need to check is_today from API
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Fetch today tasks from each project's tasks
|
|
66
|
+
for (const p of withSubs) {
|
|
67
|
+
for (const sp of p.subProjects) {
|
|
68
|
+
if (sp.task_count > 0) {
|
|
69
|
+
const tasksRes = await fetch(`/api/projects/${p.id}/sub-projects/${sp.id}/tasks`);
|
|
70
|
+
const tasks: ITask[] = await tasksRes.json();
|
|
71
|
+
for (const t of tasks) {
|
|
72
|
+
if (t.is_today) {
|
|
73
|
+
allToday.push({ ...t, projectName: p.name, subProjectName: sp.name });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
setTodayTasks(allToday);
|
|
26
80
|
setLoading(false);
|
|
27
81
|
}, []);
|
|
28
82
|
|
|
29
83
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
}, [
|
|
84
|
+
fetchData();
|
|
85
|
+
}, [fetchData]);
|
|
86
|
+
|
|
87
|
+
const handleTabChange = (newTab: DashboardTab) => {
|
|
88
|
+
setTab(newTab);
|
|
89
|
+
localStorage.setItem('im-dashboard-tab', newTab);
|
|
90
|
+
};
|
|
32
91
|
|
|
33
92
|
const handleCreate = async (e: React.FormEvent) => {
|
|
34
93
|
e.preventDefault();
|
|
@@ -37,62 +96,80 @@ export default function Dashboard() {
|
|
|
37
96
|
const res = await fetch('/api/projects', {
|
|
38
97
|
method: 'POST',
|
|
39
98
|
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
body: JSON.stringify({ name: name.trim(), description: description.trim() }),
|
|
99
|
+
body: JSON.stringify({ name: name.trim(), description: description.trim(), project_path: projectPath.trim() || undefined }),
|
|
41
100
|
});
|
|
42
101
|
|
|
43
102
|
if (res.ok) {
|
|
44
103
|
const project = await res.json();
|
|
45
104
|
setName('');
|
|
46
105
|
setDescription('');
|
|
106
|
+
setProjectPath('');
|
|
47
107
|
setShowForm(false);
|
|
48
108
|
router.push(`/projects/${project.id}`);
|
|
49
109
|
}
|
|
50
110
|
};
|
|
51
111
|
|
|
52
|
-
const
|
|
112
|
+
const handleDeleteClick = (id: string, e: React.MouseEvent) => {
|
|
53
113
|
e.stopPropagation();
|
|
54
|
-
|
|
114
|
+
setDeleteTarget(id);
|
|
115
|
+
};
|
|
55
116
|
|
|
56
|
-
|
|
57
|
-
|
|
117
|
+
const handleDeleteConfirm = async () => {
|
|
118
|
+
if (!deleteTarget) return;
|
|
119
|
+
await fetch(`/api/projects/${deleteTarget}`, { method: 'DELETE' });
|
|
120
|
+
setDeleteTarget(null);
|
|
121
|
+
fetchData();
|
|
58
122
|
};
|
|
59
123
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
// Filter sub-projects based on tab
|
|
125
|
+
const getVisibleCards = (): { sp: ISubProjectWithStats; projectName: string; projectId: string }[] => {
|
|
126
|
+
const cards: { sp: ISubProjectWithStats; projectName: string; projectId: string }[] = [];
|
|
127
|
+
for (const p of projects) {
|
|
128
|
+
for (const sp of p.subProjects) {
|
|
129
|
+
if (tab === 'active') {
|
|
130
|
+
if (sp.active_count > 0 || sp.problem_count > 0) {
|
|
131
|
+
cards.push({ sp, projectName: p.name, projectId: p.id });
|
|
132
|
+
}
|
|
133
|
+
} else if (tab === 'all') {
|
|
134
|
+
cards.push({ sp, projectName: p.name, projectId: p.id });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Sort: active cards first
|
|
139
|
+
cards.sort((a, b) => (b.sp.active_count + b.sp.problem_count) - (a.sp.active_count + a.sp.problem_count));
|
|
140
|
+
return cards;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const STATUS_ICONS: Record<string, string> = {
|
|
144
|
+
idea: '\u{1F4A1}', writing: '\u{270F}\u{FE0F}', submitted: '\u{1F680}',
|
|
145
|
+
testing: '\u{1F9EA}', done: '\u{2705}', problem: '\u{1F534}',
|
|
66
146
|
};
|
|
67
147
|
|
|
68
148
|
return (
|
|
69
|
-
<div className="min-h-screen p-8 max-w-
|
|
70
|
-
<header className="flex items-center justify-between mb-
|
|
149
|
+
<div className="min-h-screen p-8 max-w-5xl mx-auto">
|
|
150
|
+
<header className="flex items-center justify-between mb-6">
|
|
71
151
|
<div>
|
|
72
|
-
<h1 className="text-
|
|
73
|
-
IM <span className="text-muted-foreground font-normal text-
|
|
152
|
+
<h1 className="text-2xl font-bold tracking-tight">
|
|
153
|
+
IM <span className="text-muted-foreground font-normal text-sm ml-2">Idea Manager v2</span>
|
|
74
154
|
</h1>
|
|
75
|
-
<p className="text-muted-foreground mt-1 text-sm">
|
|
76
|
-
자유롭게 아이디어를 쏟아내면, AI가 구조화해드립니다
|
|
77
|
-
</p>
|
|
78
155
|
</div>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
156
|
+
<div className="flex items-center gap-3">
|
|
157
|
+
<TabBar value={tab} onChange={handleTabChange} />
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => setShowForm(!showForm)}
|
|
160
|
+
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
|
|
161
|
+
transition-colors font-medium text-sm"
|
|
162
|
+
>
|
|
163
|
+
+ Project
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
86
166
|
</header>
|
|
87
167
|
|
|
88
168
|
{showForm && (
|
|
89
|
-
<form
|
|
90
|
-
onSubmit={handleCreate}
|
|
91
|
-
className="mb-8 p-5 bg-card rounded-lg border border-border"
|
|
92
|
-
>
|
|
169
|
+
<form onSubmit={handleCreate} className="mb-6 p-5 bg-card rounded-lg border border-border">
|
|
93
170
|
<input
|
|
94
171
|
type="text"
|
|
95
|
-
placeholder="
|
|
172
|
+
placeholder="Project name"
|
|
96
173
|
value={name}
|
|
97
174
|
onChange={(e) => setName(e.target.value)}
|
|
98
175
|
className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
|
|
@@ -101,75 +178,170 @@ export default function Dashboard() {
|
|
|
101
178
|
/>
|
|
102
179
|
<input
|
|
103
180
|
type="text"
|
|
104
|
-
placeholder="
|
|
181
|
+
placeholder="Description (optional)"
|
|
105
182
|
value={description}
|
|
106
183
|
onChange={(e) => setDescription(e.target.value)}
|
|
107
|
-
className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-
|
|
184
|
+
className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
|
|
108
185
|
focus:border-primary focus:outline-none text-foreground"
|
|
109
186
|
/>
|
|
110
|
-
<div className="
|
|
187
|
+
<div className="mb-4">
|
|
111
188
|
<button
|
|
112
189
|
type="button"
|
|
113
|
-
onClick={() =>
|
|
114
|
-
className="
|
|
190
|
+
onClick={() => setShowDirPicker(true)}
|
|
191
|
+
className="w-full bg-input border border-border rounded-lg px-4 py-2.5
|
|
192
|
+
text-left text-sm hover:border-primary transition-colors"
|
|
115
193
|
>
|
|
116
|
-
|
|
194
|
+
{projectPath ? (
|
|
195
|
+
<span className="font-mono text-foreground">{projectPath}</span>
|
|
196
|
+
) : (
|
|
197
|
+
<span className="text-muted-foreground">Project folder (optional)</span>
|
|
198
|
+
)}
|
|
117
199
|
</button>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex gap-2 justify-end">
|
|
202
|
+
<button type="button" onClick={() => setShowForm(false)}
|
|
203
|
+
className="px-4 py-2 text-muted-foreground hover:text-foreground transition-colors text-sm">
|
|
204
|
+
Cancel
|
|
205
|
+
</button>
|
|
206
|
+
<button type="submit"
|
|
207
|
+
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors text-sm">
|
|
208
|
+
Create
|
|
124
209
|
</button>
|
|
125
210
|
</div>
|
|
126
211
|
</form>
|
|
127
212
|
)}
|
|
128
213
|
|
|
129
214
|
{loading ? (
|
|
130
|
-
<div className="text-center text-muted-foreground py-20"
|
|
131
|
-
) :
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
</
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
{
|
|
152
|
-
</
|
|
153
|
-
{project.description && (
|
|
154
|
-
<p className="text-muted-foreground text-sm mt-1">{project.description}</p>
|
|
155
|
-
)}
|
|
156
|
-
<p className="text-muted-foreground text-xs mt-2">
|
|
157
|
-
수정일 {formatDate(project.updated_at)}
|
|
158
|
-
</p>
|
|
215
|
+
<div className="text-center text-muted-foreground py-20">Loading...</div>
|
|
216
|
+
) : tab === 'today' ? (
|
|
217
|
+
/* Today tab */
|
|
218
|
+
todayTasks.length === 0 ? (
|
|
219
|
+
<div className="text-center py-20 text-muted-foreground">
|
|
220
|
+
<p className="text-lg mb-2">No tasks marked for today</p>
|
|
221
|
+
<p className="text-sm">Mark tasks with the Today button in task detail</p>
|
|
222
|
+
</div>
|
|
223
|
+
) : (
|
|
224
|
+
<div className="space-y-2">
|
|
225
|
+
{todayTasks.map((task) => (
|
|
226
|
+
<div
|
|
227
|
+
key={task.id}
|
|
228
|
+
onClick={() => router.push(`/projects/${task.project_id}?sub=${task.sub_project_id}&task=${task.id}`)}
|
|
229
|
+
className="flex items-center gap-3 p-3 bg-card hover:bg-card-hover border border-border
|
|
230
|
+
rounded-lg cursor-pointer transition-colors"
|
|
231
|
+
>
|
|
232
|
+
<span className="text-sm">{STATUS_ICONS[task.status]}</span>
|
|
233
|
+
<div className="flex-1 min-w-0">
|
|
234
|
+
<span className="text-sm font-medium">{task.title}</span>
|
|
235
|
+
<span className="text-xs text-muted-foreground ml-2">
|
|
236
|
+
{task.projectName} / {task.subProjectName}
|
|
237
|
+
</span>
|
|
159
238
|
</div>
|
|
160
|
-
<button
|
|
161
|
-
onClick={(e) => handleDelete(project.id, e)}
|
|
162
|
-
className="text-muted-foreground hover:text-destructive transition-colors opacity-0
|
|
163
|
-
group-hover:opacity-100 p-1 text-sm"
|
|
164
|
-
title="프로젝트 삭제"
|
|
165
|
-
>
|
|
166
|
-
삭제
|
|
167
|
-
</button>
|
|
168
239
|
</div>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
) : (
|
|
244
|
+
/* Active / All tabs */
|
|
245
|
+
<>
|
|
246
|
+
{/* Project headers for All tab */}
|
|
247
|
+
{tab === 'all' ? (
|
|
248
|
+
projects.length === 0 ? (
|
|
249
|
+
<div className="text-center py-20">
|
|
250
|
+
<p className="text-muted-foreground text-lg mb-2">No projects yet</p>
|
|
251
|
+
<p className="text-muted-foreground text-sm">Click + Project to get started</p>
|
|
252
|
+
</div>
|
|
253
|
+
) : (
|
|
254
|
+
<div className="space-y-6">
|
|
255
|
+
{projects.map((project) => (
|
|
256
|
+
<div key={project.id}>
|
|
257
|
+
<div className="flex items-center justify-between mb-3">
|
|
258
|
+
<div
|
|
259
|
+
className="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors"
|
|
260
|
+
onClick={() => router.push(`/projects/${project.id}`)}
|
|
261
|
+
>
|
|
262
|
+
<h2 className="text-sm font-semibold">{project.name}</h2>
|
|
263
|
+
{project.project_path && (
|
|
264
|
+
<span className="text-xs text-muted-foreground font-mono truncate max-w-48">
|
|
265
|
+
{project.project_path}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
<button
|
|
270
|
+
onClick={(e) => handleDeleteClick(project.id, e)}
|
|
271
|
+
className="text-xs text-muted-foreground hover:text-destructive transition-colors"
|
|
272
|
+
>
|
|
273
|
+
Delete
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
{project.subProjects.length === 0 ? (
|
|
277
|
+
<div className="text-xs text-muted-foreground py-4 text-center border border-dashed border-border rounded-lg">
|
|
278
|
+
No sub-projects.{' '}
|
|
279
|
+
<span
|
|
280
|
+
className="text-primary cursor-pointer hover:underline"
|
|
281
|
+
onClick={() => router.push(`/projects/${project.id}`)}
|
|
282
|
+
>
|
|
283
|
+
Open project
|
|
284
|
+
</span>{' '}
|
|
285
|
+
to add one.
|
|
286
|
+
</div>
|
|
287
|
+
) : (
|
|
288
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
289
|
+
{project.subProjects.map((sp) => (
|
|
290
|
+
<SubProjectCard
|
|
291
|
+
key={sp.id}
|
|
292
|
+
subProject={sp}
|
|
293
|
+
projectName={project.name}
|
|
294
|
+
onClick={() => router.push(`/projects/${project.id}?sub=${sp.id}`)}
|
|
295
|
+
/>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
) : (
|
|
304
|
+
/* Active tab */
|
|
305
|
+
(() => {
|
|
306
|
+
const cards = getVisibleCards();
|
|
307
|
+
return cards.length === 0 ? (
|
|
308
|
+
<div className="text-center py-20 text-muted-foreground">
|
|
309
|
+
<p className="text-lg mb-2">No active tasks</p>
|
|
310
|
+
<p className="text-sm">Submit tasks to see them here</p>
|
|
311
|
+
</div>
|
|
312
|
+
) : (
|
|
313
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
314
|
+
{cards.map(({ sp, projectName, projectId }) => (
|
|
315
|
+
<SubProjectCard
|
|
316
|
+
key={sp.id}
|
|
317
|
+
subProject={sp}
|
|
318
|
+
projectName={projectName}
|
|
319
|
+
onClick={() => router.push(`/projects/${projectId}?sub=${sp.id}`)}
|
|
320
|
+
/>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
})()
|
|
325
|
+
)}
|
|
326
|
+
</>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{showDirPicker && (
|
|
330
|
+
<DirectoryPicker
|
|
331
|
+
onSelect={(path) => { setProjectPath(path); setShowDirPicker(false); }}
|
|
332
|
+
onCancel={() => setShowDirPicker(false)}
|
|
333
|
+
/>
|
|
172
334
|
)}
|
|
335
|
+
|
|
336
|
+
<ConfirmDialog
|
|
337
|
+
open={!!deleteTarget}
|
|
338
|
+
title="Delete project?"
|
|
339
|
+
description="This will permanently delete the project and all its data."
|
|
340
|
+
confirmLabel="Delete"
|
|
341
|
+
variant="danger"
|
|
342
|
+
onConfirm={handleDeleteConfirm}
|
|
343
|
+
onCancel={() => setDeleteTarget(null)}
|
|
344
|
+
/>
|
|
173
345
|
</div>
|
|
174
346
|
);
|
|
175
347
|
}
|