@polderlabs/bizar 2.6.1 → 3.0.1
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/cli/bin.mjs +158 -130
- package/cli/plan.test.mjs +2331 -0
- package/cli/service.mjs +309 -0
- package/package.json +19 -27
- package/cli/dashboard/api.mjs +0 -473
- package/cli/dashboard/browser.mjs +0 -40
- package/cli/dashboard/server.mjs +0 -366
- package/cli/dashboard/state.mjs +0 -438
- package/cli/dashboard/tasks-store.mjs +0 -203
- package/cli/dashboard/watcher.mjs +0 -81
- package/cli/dashboard.mjs +0 -97
- package/dist/assets/index-BVvY22Gt.css +0 -1
- package/dist/assets/index-CO3c8O32.js +0 -285
- package/dist/assets/index-CO3c8O32.js.map +0 -1
- package/dist/index.html +0 -18
- package/src/App.tsx +0 -233
- package/src/components/Button.tsx +0 -55
- package/src/components/Card.tsx +0 -40
- package/src/components/EmptyState.tsx +0 -30
- package/src/components/Modal.tsx +0 -137
- package/src/components/Spinner.tsx +0 -19
- package/src/components/StatusBadge.tsx +0 -25
- package/src/components/Tag.tsx +0 -28
- package/src/components/Toast.tsx +0 -142
- package/src/components/Topbar.tsx +0 -88
- package/src/index.html +0 -17
- package/src/lib/api.ts +0 -71
- package/src/lib/markdown.tsx +0 -59
- package/src/lib/types.ts +0 -200
- package/src/lib/utils.ts +0 -79
- package/src/lib/ws.ts +0 -132
- package/src/main.tsx +0 -12
- package/src/styles/main.css +0 -2324
- package/src/views/Agents.tsx +0 -199
- package/src/views/Chat.tsx +0 -255
- package/src/views/Config.tsx +0 -250
- package/src/views/Overview.tsx +0 -267
- package/src/views/Plans.tsx +0 -667
- package/src/views/Projects.tsx +0 -155
- package/src/views/Settings.tsx +0 -253
- package/src/views/Tasks.tsx +0 -567
- package/tsconfig.json +0 -23
- package/vite.config.ts +0 -24
package/src/views/Tasks.tsx
DELETED
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
// src/views/Tasks.tsx — Personal Task Kanban Board.
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
CheckSquare,
|
|
5
|
-
Plus,
|
|
6
|
-
Pencil,
|
|
7
|
-
Trash2,
|
|
8
|
-
ChevronLeft,
|
|
9
|
-
ChevronRight,
|
|
10
|
-
Search,
|
|
11
|
-
X as XIcon,
|
|
12
|
-
} from 'lucide-react';
|
|
13
|
-
import { Button } from '../components/Button';
|
|
14
|
-
import { Card, CardTitle } from '../components/Card';
|
|
15
|
-
import { EmptyState } from '../components/EmptyState';
|
|
16
|
-
import { Spinner } from '../components/Spinner';
|
|
17
|
-
import { StatusBadge } from '../components/StatusBadge';
|
|
18
|
-
import { Tag } from '../components/Tag';
|
|
19
|
-
import { useModal } from '../components/Modal';
|
|
20
|
-
import { useToast } from '../components/Toast';
|
|
21
|
-
import { api } from '../lib/api';
|
|
22
|
-
import { cn, formatRelative, priorityColors } from '../lib/utils';
|
|
23
|
-
import type { Settings, Snapshot, Task } from '../lib/types';
|
|
24
|
-
|
|
25
|
-
type Props = {
|
|
26
|
-
snapshot: Snapshot;
|
|
27
|
-
settings: Settings;
|
|
28
|
-
activeTab: string;
|
|
29
|
-
setActiveTab: (id: string) => void;
|
|
30
|
-
refreshSnapshot: () => Promise<void>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type Column = {
|
|
34
|
-
id: Task['status'] | string;
|
|
35
|
-
label: string;
|
|
36
|
-
kind: 'info' | 'accent' | 'success';
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const COLUMNS: Column[] = [
|
|
40
|
-
{ id: 'queued', label: 'Queued', kind: 'info' },
|
|
41
|
-
{ id: 'doing', label: 'Doing', kind: 'accent' },
|
|
42
|
-
{ id: 'done', label: 'Done', kind: 'success' },
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const PRIORITIES = ['low', 'normal', 'high'] as const;
|
|
46
|
-
type Priority = (typeof PRIORITIES)[number];
|
|
47
|
-
|
|
48
|
-
export function Tasks({ snapshot }: Props) {
|
|
49
|
-
const toast = useToast();
|
|
50
|
-
const modal = useModal();
|
|
51
|
-
const [tasks, setTasks] = useState<Task[]>(snapshot.tasks || []);
|
|
52
|
-
const [loading, setLoading] = useState(!snapshot.tasks);
|
|
53
|
-
const [filter, setFilter] = useState('');
|
|
54
|
-
const [focusedId, setFocusedId] = useState<string | null>(null);
|
|
55
|
-
|
|
56
|
-
const reload = async () => {
|
|
57
|
-
try {
|
|
58
|
-
const data = await api.get<Task[]>('/tasks');
|
|
59
|
-
setTasks(Array.isArray(data) ? data : []);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
toast.error(`Tasks load failed: ${(err as Error).message}`);
|
|
62
|
-
} finally {
|
|
63
|
-
setLoading(false);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (snapshot.tasks?.length || snapshot.tasks) {
|
|
69
|
-
setTasks(snapshot.tasks || []);
|
|
70
|
-
setLoading(false);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
reload();
|
|
74
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
-
}, [snapshot.tasks]);
|
|
76
|
-
|
|
77
|
-
const filtered = useMemo(() => {
|
|
78
|
-
if (!filter.trim()) return tasks;
|
|
79
|
-
const q = filter.toLowerCase();
|
|
80
|
-
return tasks.filter((t) => {
|
|
81
|
-
const title = (t.title || '').toLowerCase();
|
|
82
|
-
const desc = (t.description || '').toLowerCase();
|
|
83
|
-
const tags = (t.tags || []).join(' ').toLowerCase();
|
|
84
|
-
return title.includes(q) || desc.includes(q) || tags.includes(q);
|
|
85
|
-
});
|
|
86
|
-
}, [tasks, filter]);
|
|
87
|
-
|
|
88
|
-
const moveTask = async (taskId: string, newStatus: string) => {
|
|
89
|
-
const t = tasks.find((x) => x.id === taskId);
|
|
90
|
-
if (!t) return;
|
|
91
|
-
const prev = t.status;
|
|
92
|
-
setTasks((cur) =>
|
|
93
|
-
cur.map((x) => (x.id === taskId ? { ...x, status: newStatus } : x)),
|
|
94
|
-
);
|
|
95
|
-
try {
|
|
96
|
-
await api.patch(`/tasks/${encodeURIComponent(taskId)}/status`, {
|
|
97
|
-
status: newStatus,
|
|
98
|
-
});
|
|
99
|
-
toast.success(`Moved to ${newStatus}.`, 1500);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
// Revert
|
|
102
|
-
setTasks((cur) =>
|
|
103
|
-
cur.map((x) => (x.id === taskId ? { ...x, status: prev } : x)),
|
|
104
|
-
);
|
|
105
|
-
toast.error(`Move failed: ${(err as Error).message}`);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const deleteTask = async (taskId: string) => {
|
|
110
|
-
if (
|
|
111
|
-
// eslint-disable-next-line no-alert
|
|
112
|
-
!confirm('Delete this task?')
|
|
113
|
-
)
|
|
114
|
-
return;
|
|
115
|
-
try {
|
|
116
|
-
await api.del(`/tasks/${encodeURIComponent(taskId)}`);
|
|
117
|
-
setTasks((cur) => cur.filter((t) => t.id !== taskId));
|
|
118
|
-
toast.success('Task deleted.', 1500);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
toast.error(`Delete failed: ${(err as Error).message}`);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Keyboard shortcuts
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
const handler = (e: KeyboardEvent) => {
|
|
127
|
-
const t = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
|
|
128
|
-
if (
|
|
129
|
-
t === 'input' ||
|
|
130
|
-
t === 'textarea' ||
|
|
131
|
-
(e.target as HTMLElement)?.isContentEditable ||
|
|
132
|
-
e.metaKey ||
|
|
133
|
-
e.ctrlKey ||
|
|
134
|
-
e.altKey
|
|
135
|
-
)
|
|
136
|
-
return;
|
|
137
|
-
if (e.key === 'n') {
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
openTaskModal(modal, toast, null, 'queued', reload);
|
|
140
|
-
} else if (e.key === '1') {
|
|
141
|
-
document
|
|
142
|
-
.querySelector('[data-column="queued"]')
|
|
143
|
-
?.scrollIntoView({ behavior: 'smooth' });
|
|
144
|
-
} else if (e.key === '2') {
|
|
145
|
-
document
|
|
146
|
-
.querySelector('[data-column="doing"]')
|
|
147
|
-
?.scrollIntoView({ behavior: 'smooth' });
|
|
148
|
-
} else if (e.key === '3') {
|
|
149
|
-
document
|
|
150
|
-
.querySelector('[data-column="done"]')
|
|
151
|
-
?.scrollIntoView({ behavior: 'smooth' });
|
|
152
|
-
} else if (e.key === 'e' && focusedId) {
|
|
153
|
-
const task = tasks.find((x) => x.id === focusedId);
|
|
154
|
-
if (task) openTaskModal(modal, toast, task, task.status, reload);
|
|
155
|
-
} else if ((e.key === 'Delete' || e.key === 'Backspace') && focusedId) {
|
|
156
|
-
e.preventDefault();
|
|
157
|
-
deleteTask(focusedId);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
document.addEventListener('keydown', handler);
|
|
161
|
-
return () => document.removeEventListener('keydown', handler);
|
|
162
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
-
}, [focusedId, tasks]);
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
<div className="view view-tasks">
|
|
167
|
-
<header className="view-header">
|
|
168
|
-
<div className="view-header-text">
|
|
169
|
-
<h2 className="view-title">
|
|
170
|
-
<CheckSquare size={18} /> Tasks
|
|
171
|
-
</h2>
|
|
172
|
-
<p className="view-subtitle">
|
|
173
|
-
Personal kanban. Press <kbd>n</kbd> for new, <kbd>1-3</kbd> to jump columns, <kbd>e</kbd> edit, <kbd>Del</kbd> delete.
|
|
174
|
-
</p>
|
|
175
|
-
</div>
|
|
176
|
-
<div className="view-actions">
|
|
177
|
-
<div className="search-input">
|
|
178
|
-
<Search size={14} />
|
|
179
|
-
<input
|
|
180
|
-
className="input"
|
|
181
|
-
type="text"
|
|
182
|
-
placeholder="Search…"
|
|
183
|
-
value={filter}
|
|
184
|
-
onChange={(e) => setFilter(e.target.value)}
|
|
185
|
-
/>
|
|
186
|
-
{filter && (
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
className="icon-btn"
|
|
190
|
-
aria-label="Clear search"
|
|
191
|
-
onClick={() => setFilter('')}
|
|
192
|
-
>
|
|
193
|
-
<XIcon size={12} />
|
|
194
|
-
</button>
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
<Button
|
|
198
|
-
variant="primary"
|
|
199
|
-
size="sm"
|
|
200
|
-
onClick={() => openTaskModal(modal, toast, null, 'queued', reload)}
|
|
201
|
-
>
|
|
202
|
-
<Plus size={14} /> Add task
|
|
203
|
-
</Button>
|
|
204
|
-
</div>
|
|
205
|
-
</header>
|
|
206
|
-
|
|
207
|
-
{loading ? (
|
|
208
|
-
<div className="view-loading">
|
|
209
|
-
<Spinner size="lg" />
|
|
210
|
-
</div>
|
|
211
|
-
) : (
|
|
212
|
-
<div className="kanban">
|
|
213
|
-
{COLUMNS.map((col) => (
|
|
214
|
-
<KanbanColumn
|
|
215
|
-
key={col.id}
|
|
216
|
-
column={col}
|
|
217
|
-
tasks={filtered.filter((t) => t.status === col.id)}
|
|
218
|
-
focusedId={focusedId}
|
|
219
|
-
onFocus={setFocusedId}
|
|
220
|
-
onMove={moveTask}
|
|
221
|
-
onDelete={deleteTask}
|
|
222
|
-
onEdit={(t) => openTaskModal(modal, toast, t, t.status, reload)}
|
|
223
|
-
onAdd={() => openTaskModal(modal, toast, null, col.id, reload)}
|
|
224
|
-
/>
|
|
225
|
-
))}
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function KanbanColumn({
|
|
233
|
-
column,
|
|
234
|
-
tasks,
|
|
235
|
-
focusedId,
|
|
236
|
-
onFocus,
|
|
237
|
-
onMove,
|
|
238
|
-
onDelete,
|
|
239
|
-
onEdit,
|
|
240
|
-
onAdd,
|
|
241
|
-
}: {
|
|
242
|
-
column: Column;
|
|
243
|
-
tasks: Task[];
|
|
244
|
-
focusedId: string | null;
|
|
245
|
-
onFocus: (id: string | null) => void;
|
|
246
|
-
onMove: (id: string, status: string) => void;
|
|
247
|
-
onDelete: (id: string) => void;
|
|
248
|
-
onEdit: (task: Task) => void;
|
|
249
|
-
onAdd: () => void;
|
|
250
|
-
}) {
|
|
251
|
-
const [dragOver, setDragOver] = useState(false);
|
|
252
|
-
return (
|
|
253
|
-
<div
|
|
254
|
-
className={cn('kanban-column', dragOver && 'kanban-column-drop')}
|
|
255
|
-
data-column={column.id}
|
|
256
|
-
onDragOver={(e) => {
|
|
257
|
-
e.preventDefault();
|
|
258
|
-
setDragOver(true);
|
|
259
|
-
}}
|
|
260
|
-
onDragLeave={() => setDragOver(false)}
|
|
261
|
-
onDrop={(e) => {
|
|
262
|
-
e.preventDefault();
|
|
263
|
-
setDragOver(false);
|
|
264
|
-
const id = e.dataTransfer.getData('text/task-id');
|
|
265
|
-
if (id) onMove(id, column.id);
|
|
266
|
-
}}
|
|
267
|
-
>
|
|
268
|
-
<div className="kanban-col-header">
|
|
269
|
-
<CardTitle>
|
|
270
|
-
<StatusBadge kind={column.kind} dot>
|
|
271
|
-
{column.label}
|
|
272
|
-
</StatusBadge>
|
|
273
|
-
</CardTitle>
|
|
274
|
-
<span className="kanban-col-count tabular-nums">{tasks.length}</span>
|
|
275
|
-
</div>
|
|
276
|
-
<div className="kanban-col-body">
|
|
277
|
-
{tasks.length === 0 ? (
|
|
278
|
-
<div className="kanban-empty">No tasks</div>
|
|
279
|
-
) : (
|
|
280
|
-
tasks.map((t) => (
|
|
281
|
-
<TaskCard
|
|
282
|
-
key={t.id}
|
|
283
|
-
task={t}
|
|
284
|
-
focused={focusedId === t.id}
|
|
285
|
-
onFocus={() =>
|
|
286
|
-
onFocus(focusedId === t.id ? null : t.id)
|
|
287
|
-
}
|
|
288
|
-
onMove={(dir) => {
|
|
289
|
-
const idx = COLUMNS.findIndex((c) => c.id === t.status);
|
|
290
|
-
const next = idx + dir;
|
|
291
|
-
if (next >= 0 && next < COLUMNS.length)
|
|
292
|
-
onMove(t.id, COLUMNS[next].id);
|
|
293
|
-
}}
|
|
294
|
-
onEdit={() => onEdit(t)}
|
|
295
|
-
onDelete={() => onDelete(t.id)}
|
|
296
|
-
/>
|
|
297
|
-
))
|
|
298
|
-
)}
|
|
299
|
-
</div>
|
|
300
|
-
<footer className="kanban-col-footer">
|
|
301
|
-
<Button variant="ghost" size="sm" onClick={onAdd} className="w-full">
|
|
302
|
-
<Plus size={12} /> Add task
|
|
303
|
-
</Button>
|
|
304
|
-
</footer>
|
|
305
|
-
</div>
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function TaskCard({
|
|
310
|
-
task,
|
|
311
|
-
focused,
|
|
312
|
-
onFocus,
|
|
313
|
-
onMove,
|
|
314
|
-
onEdit,
|
|
315
|
-
onDelete,
|
|
316
|
-
}: {
|
|
317
|
-
task: Task;
|
|
318
|
-
focused: boolean;
|
|
319
|
-
onFocus: () => void;
|
|
320
|
-
onMove: (dir: -1 | 1) => void;
|
|
321
|
-
onEdit: () => void;
|
|
322
|
-
onDelete: () => void;
|
|
323
|
-
}) {
|
|
324
|
-
return (
|
|
325
|
-
<div
|
|
326
|
-
className={cn(
|
|
327
|
-
'task-card',
|
|
328
|
-
`priority-${task.priority}`,
|
|
329
|
-
focused && 'task-card-focused',
|
|
330
|
-
)}
|
|
331
|
-
data-task-id={task.id}
|
|
332
|
-
draggable
|
|
333
|
-
onDragStart={(e) => {
|
|
334
|
-
e.dataTransfer.setData('text/task-id', task.id);
|
|
335
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
336
|
-
}}
|
|
337
|
-
onClick={onFocus}
|
|
338
|
-
>
|
|
339
|
-
<div className="task-card-head">
|
|
340
|
-
<span
|
|
341
|
-
className="priority-dot"
|
|
342
|
-
style={{ background: priorityColors[task.priority] || 'var(--info)' }}
|
|
343
|
-
/>
|
|
344
|
-
<div className="task-card-title">{task.title}</div>
|
|
345
|
-
</div>
|
|
346
|
-
{task.description && (
|
|
347
|
-
<div className="task-card-desc">{task.description.slice(0, 160)}</div>
|
|
348
|
-
)}
|
|
349
|
-
{task.tags && task.tags.length > 0 && (
|
|
350
|
-
<div className="task-card-tags">
|
|
351
|
-
{task.tags.map((t) => (
|
|
352
|
-
<Tag key={t}>{t}</Tag>
|
|
353
|
-
))}
|
|
354
|
-
</div>
|
|
355
|
-
)}
|
|
356
|
-
<div className="task-card-footer">
|
|
357
|
-
<span className="task-card-time tabular-nums muted">
|
|
358
|
-
{formatRelative(task.updatedAt || task.createdAt)}
|
|
359
|
-
</span>
|
|
360
|
-
<div className="task-card-actions" onClick={(e) => e.stopPropagation()}>
|
|
361
|
-
<button
|
|
362
|
-
type="button"
|
|
363
|
-
className="icon-btn"
|
|
364
|
-
aria-label="Move left"
|
|
365
|
-
title="Move left"
|
|
366
|
-
onClick={() => onMove(-1)}
|
|
367
|
-
>
|
|
368
|
-
<ChevronLeft size={14} />
|
|
369
|
-
</button>
|
|
370
|
-
<button
|
|
371
|
-
type="button"
|
|
372
|
-
className="icon-btn"
|
|
373
|
-
aria-label="Edit"
|
|
374
|
-
title="Edit (e)"
|
|
375
|
-
onClick={onEdit}
|
|
376
|
-
>
|
|
377
|
-
<Pencil size={14} />
|
|
378
|
-
</button>
|
|
379
|
-
<button
|
|
380
|
-
type="button"
|
|
381
|
-
className="icon-btn icon-btn-danger"
|
|
382
|
-
aria-label="Delete"
|
|
383
|
-
title="Delete"
|
|
384
|
-
onClick={onDelete}
|
|
385
|
-
>
|
|
386
|
-
<Trash2 size={14} />
|
|
387
|
-
</button>
|
|
388
|
-
<button
|
|
389
|
-
type="button"
|
|
390
|
-
className="icon-btn"
|
|
391
|
-
aria-label="Move right"
|
|
392
|
-
title="Move right"
|
|
393
|
-
onClick={() => onMove(1)}
|
|
394
|
-
>
|
|
395
|
-
<ChevronRight size={14} />
|
|
396
|
-
</button>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
399
|
-
</div>
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function openTaskModal(
|
|
404
|
-
modal: ReturnType<typeof useModal>,
|
|
405
|
-
toast: ReturnType<typeof useToast>,
|
|
406
|
-
task: Task | null,
|
|
407
|
-
initialStatus: string,
|
|
408
|
-
reload: () => Promise<void>,
|
|
409
|
-
) {
|
|
410
|
-
let titleEl: HTMLInputElement | null = null;
|
|
411
|
-
let descEl: HTMLTextAreaElement | null = null;
|
|
412
|
-
let tagsEl: HTMLInputElement | null = null;
|
|
413
|
-
let priorityEl: HTMLDivElement | null = null;
|
|
414
|
-
let statusEl: HTMLSelectElement | null = null;
|
|
415
|
-
|
|
416
|
-
const submit = async () => {
|
|
417
|
-
const title = titleEl?.value.trim() || '';
|
|
418
|
-
if (!title) {
|
|
419
|
-
toast.warning('Title is required.');
|
|
420
|
-
titleEl?.focus();
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const description = descEl?.value.trim() || '';
|
|
424
|
-
const priority =
|
|
425
|
-
(priorityEl?.querySelector<HTMLInputElement>(
|
|
426
|
-
'input[name="task-priority"]:checked',
|
|
427
|
-
)?.value as Priority) || 'normal';
|
|
428
|
-
const tagsRaw = tagsEl?.value.trim() || '';
|
|
429
|
-
const tags = tagsRaw
|
|
430
|
-
? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean)
|
|
431
|
-
: [];
|
|
432
|
-
const status = statusEl?.value || initialStatus;
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
if (task) {
|
|
436
|
-
await api.put(`/tasks/${encodeURIComponent(task.id)}`, {
|
|
437
|
-
title,
|
|
438
|
-
description,
|
|
439
|
-
tags,
|
|
440
|
-
priority,
|
|
441
|
-
status,
|
|
442
|
-
});
|
|
443
|
-
toast.success('Task updated.', 1500);
|
|
444
|
-
} else {
|
|
445
|
-
await api.post('/tasks', {
|
|
446
|
-
title,
|
|
447
|
-
description,
|
|
448
|
-
status,
|
|
449
|
-
tags,
|
|
450
|
-
priority,
|
|
451
|
-
});
|
|
452
|
-
toast.success('Task created.', 1500);
|
|
453
|
-
}
|
|
454
|
-
modal.close();
|
|
455
|
-
await reload();
|
|
456
|
-
} catch (err) {
|
|
457
|
-
toast.error(`Save failed: ${(err as Error).message}`);
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
modal.open({
|
|
462
|
-
title: task ? 'Edit task' : 'New task',
|
|
463
|
-
width: 520,
|
|
464
|
-
children: (
|
|
465
|
-
<div className="task-form">
|
|
466
|
-
<label className="field-label" htmlFor="task-title">
|
|
467
|
-
Title *
|
|
468
|
-
</label>
|
|
469
|
-
<input
|
|
470
|
-
ref={(el) => {
|
|
471
|
-
titleEl = el;
|
|
472
|
-
}}
|
|
473
|
-
id="task-title"
|
|
474
|
-
className="input"
|
|
475
|
-
type="text"
|
|
476
|
-
maxLength={200}
|
|
477
|
-
placeholder="What needs to be done?"
|
|
478
|
-
defaultValue={task?.title || ''}
|
|
479
|
-
autoFocus
|
|
480
|
-
/>
|
|
481
|
-
|
|
482
|
-
<label className="field-label" htmlFor="task-desc">
|
|
483
|
-
Description
|
|
484
|
-
</label>
|
|
485
|
-
<textarea
|
|
486
|
-
ref={(el) => {
|
|
487
|
-
descEl = el;
|
|
488
|
-
}}
|
|
489
|
-
id="task-desc"
|
|
490
|
-
className="textarea"
|
|
491
|
-
rows={3}
|
|
492
|
-
placeholder="Markdown supported…"
|
|
493
|
-
defaultValue={task?.description || ''}
|
|
494
|
-
/>
|
|
495
|
-
|
|
496
|
-
<div className="task-form-row">
|
|
497
|
-
<div className="task-form-field">
|
|
498
|
-
<label className="field-label" htmlFor="task-status">
|
|
499
|
-
Status
|
|
500
|
-
</label>
|
|
501
|
-
<select
|
|
502
|
-
ref={(el) => {
|
|
503
|
-
statusEl = el;
|
|
504
|
-
}}
|
|
505
|
-
id="task-status"
|
|
506
|
-
className="select"
|
|
507
|
-
defaultValue={task?.status || initialStatus}
|
|
508
|
-
>
|
|
509
|
-
{COLUMNS.map((c) => (
|
|
510
|
-
<option key={c.id} value={c.id}>
|
|
511
|
-
{c.label}
|
|
512
|
-
</option>
|
|
513
|
-
))}
|
|
514
|
-
</select>
|
|
515
|
-
</div>
|
|
516
|
-
<div className="task-form-field">
|
|
517
|
-
<label className="field-label">Priority</label>
|
|
518
|
-
<div
|
|
519
|
-
className="radio-row"
|
|
520
|
-
ref={(el) => {
|
|
521
|
-
priorityEl = el;
|
|
522
|
-
}}
|
|
523
|
-
>
|
|
524
|
-
{PRIORITIES.map((p) => (
|
|
525
|
-
<label key={p} className="radio-label">
|
|
526
|
-
<input
|
|
527
|
-
type="radio"
|
|
528
|
-
name="task-priority"
|
|
529
|
-
value={p}
|
|
530
|
-
defaultChecked={
|
|
531
|
-
(task?.priority || 'normal') === p
|
|
532
|
-
}
|
|
533
|
-
/>
|
|
534
|
-
<span className="capitalize">{p}</span>
|
|
535
|
-
</label>
|
|
536
|
-
))}
|
|
537
|
-
</div>
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
|
|
541
|
-
<label className="field-label" htmlFor="task-tags">
|
|
542
|
-
Tags
|
|
543
|
-
</label>
|
|
544
|
-
<input
|
|
545
|
-
ref={(el) => {
|
|
546
|
-
tagsEl = el;
|
|
547
|
-
}}
|
|
548
|
-
id="task-tags"
|
|
549
|
-
className="input"
|
|
550
|
-
type="text"
|
|
551
|
-
placeholder="comma-separated, e.g. bug, frontend"
|
|
552
|
-
defaultValue={(task?.tags || []).join(', ')}
|
|
553
|
-
/>
|
|
554
|
-
</div>
|
|
555
|
-
),
|
|
556
|
-
footer: (
|
|
557
|
-
<div className="modal-footer-actions">
|
|
558
|
-
<Button variant="ghost" onClick={() => modal.close()}>
|
|
559
|
-
Cancel
|
|
560
|
-
</Button>
|
|
561
|
-
<Button variant="primary" onClick={submit}>
|
|
562
|
-
{task ? 'Save' : 'Create'}
|
|
563
|
-
</Button>
|
|
564
|
-
</div>
|
|
565
|
-
),
|
|
566
|
-
});
|
|
567
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "Bundler",
|
|
7
|
-
"jsx": "react-jsx",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noUnusedLocals": false,
|
|
10
|
-
"noUnusedParameters": false,
|
|
11
|
-
"noFallthroughCasesInSwitch": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"resolveJsonModule": true,
|
|
16
|
-
"isolatedModules": true,
|
|
17
|
-
"allowSyntheticDefaultImports": true,
|
|
18
|
-
"useDefineForClassFields": true,
|
|
19
|
-
"types": ["node"]
|
|
20
|
-
},
|
|
21
|
-
"include": ["src/**/*"],
|
|
22
|
-
"exclude": ["node_modules", "dist"]
|
|
23
|
-
}
|
package/vite.config.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
-
export default defineConfig({
|
|
9
|
-
plugins: [react()],
|
|
10
|
-
root: 'src',
|
|
11
|
-
base: './',
|
|
12
|
-
build: {
|
|
13
|
-
outDir: resolve(__dirname, 'dist'),
|
|
14
|
-
emptyOutDir: true,
|
|
15
|
-
sourcemap: true,
|
|
16
|
-
rollupOptions: {
|
|
17
|
-
input: resolve(__dirname, 'src/index.html'),
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
server: {
|
|
21
|
-
port: 5173,
|
|
22
|
-
strictPort: false,
|
|
23
|
-
},
|
|
24
|
-
});
|