@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* Kanban Board Component — Trello-style drag-and-drop task board
|
|
3
|
+
* ────────────────────────────────────────────────────────────── */
|
|
4
|
+
import { h } from "preact";
|
|
5
|
+
import { useState, useCallback, useRef, useEffect } from "preact/hooks";
|
|
6
|
+
import htm from "htm";
|
|
7
|
+
import { signal, computed } from "@preact/signals";
|
|
8
|
+
import { tasksData, tasksLoaded, showToast, runOptimistic, loadTasks } from "../modules/state.js";
|
|
9
|
+
import { apiFetch } from "../modules/api.js";
|
|
10
|
+
import { haptic } from "../modules/telegram.js";
|
|
11
|
+
import { formatRelative, truncate, cloneValue } from "../modules/utils.js";
|
|
12
|
+
|
|
13
|
+
const html = htm.bind(h);
|
|
14
|
+
|
|
15
|
+
/* ─── Column definitions ─── */
|
|
16
|
+
const COLUMN_MAP = {
|
|
17
|
+
draft: ["draft"],
|
|
18
|
+
backlog: ["backlog", "open", "new", "todo"],
|
|
19
|
+
inProgress: ["in-progress", "inprogress", "working", "active", "assigned"],
|
|
20
|
+
inReview: ["in-review", "inreview", "review", "pr-open", "pr-review"],
|
|
21
|
+
done: ["done", "completed", "closed", "merged", "cancelled"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const COLUMNS = [
|
|
25
|
+
{ id: "draft", title: "Drafts", icon: "\u{1F4DD}", color: "var(--color-warning, #f59e0b)" },
|
|
26
|
+
{ id: "backlog", title: "Backlog", icon: "\u{1F4CB}", color: "var(--text-secondary)" },
|
|
27
|
+
{ id: "inProgress", title: "In Progress", icon: "\u{1F528}", color: "var(--color-inprogress, #3b82f6)" },
|
|
28
|
+
{ id: "inReview", title: "In Review", icon: "\u{1F440}", color: "var(--color-inreview, #f59e0b)" },
|
|
29
|
+
{ id: "done", title: "Done", icon: "\u2705", color: "var(--color-done, #22c55e)" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const COLUMN_TO_STATUS = {
|
|
33
|
+
draft: "draft",
|
|
34
|
+
backlog: "todo",
|
|
35
|
+
inProgress: "inprogress",
|
|
36
|
+
inReview: "inreview",
|
|
37
|
+
done: "done",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const PRIORITY_COLORS = {
|
|
41
|
+
critical: "var(--color-critical, #dc2626)",
|
|
42
|
+
high: "var(--color-high, #f59e0b)",
|
|
43
|
+
medium: "var(--color-medium, #3b82f6)",
|
|
44
|
+
low: "var(--color-low, #8b95a2)",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const PRIORITY_LABELS = {
|
|
48
|
+
critical: "CRIT",
|
|
49
|
+
high: "HIGH",
|
|
50
|
+
medium: "MED",
|
|
51
|
+
low: "LOW",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function getColumnForStatus(status) {
|
|
55
|
+
const s = (status || "").toLowerCase();
|
|
56
|
+
for (const [col, statuses] of Object.entries(COLUMN_MAP)) {
|
|
57
|
+
if (statuses.includes(s)) return col;
|
|
58
|
+
}
|
|
59
|
+
return "backlog";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getTaskTags(task) {
|
|
63
|
+
if (!task) return [];
|
|
64
|
+
const raw = Array.isArray(task.tags) && task.tags.length
|
|
65
|
+
? task.tags
|
|
66
|
+
: Array.isArray(task?.meta?.tags)
|
|
67
|
+
? task.meta.tags
|
|
68
|
+
: [];
|
|
69
|
+
return raw.filter((tag) => String(tag || "").trim().toLowerCase() !== "draft");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getTaskBaseBranch(task) {
|
|
73
|
+
if (!task) return "";
|
|
74
|
+
return (
|
|
75
|
+
task.baseBranch ||
|
|
76
|
+
task.base_branch ||
|
|
77
|
+
task.meta?.baseBranch ||
|
|
78
|
+
task.meta?.base_branch ||
|
|
79
|
+
""
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ─── Derived column data ─── */
|
|
84
|
+
const columnData = computed(() => {
|
|
85
|
+
const tasks = tasksData.value || [];
|
|
86
|
+
const cols = {};
|
|
87
|
+
for (const col of COLUMNS) {
|
|
88
|
+
cols[col.id] = [];
|
|
89
|
+
}
|
|
90
|
+
for (const task of tasks) {
|
|
91
|
+
const col = getColumnForStatus(task.status);
|
|
92
|
+
if (cols[col]) cols[col].push(task);
|
|
93
|
+
}
|
|
94
|
+
return cols;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/* ─── Drag state (module-level signals) ─── */
|
|
98
|
+
const dragTaskId = signal(null);
|
|
99
|
+
const dragOverCol = signal(null);
|
|
100
|
+
|
|
101
|
+
/* ─── Touch drag state ─── */
|
|
102
|
+
const touchDragId = signal(null);
|
|
103
|
+
const touchOverCol = signal(null);
|
|
104
|
+
let _touchClone = null;
|
|
105
|
+
let _touchStartX = 0;
|
|
106
|
+
let _touchStartY = 0;
|
|
107
|
+
let _touchMoved = false;
|
|
108
|
+
|
|
109
|
+
/* ─── Touch drag helpers ─── */
|
|
110
|
+
|
|
111
|
+
function _createTouchClone(el) {
|
|
112
|
+
const rect = el.getBoundingClientRect();
|
|
113
|
+
const clone = el.cloneNode(true);
|
|
114
|
+
clone.className = "kanban-card touch-drag-clone";
|
|
115
|
+
clone.style.position = "fixed";
|
|
116
|
+
clone.style.width = rect.width + "px";
|
|
117
|
+
clone.style.left = rect.left + "px";
|
|
118
|
+
clone.style.top = rect.top + "px";
|
|
119
|
+
clone.style.zIndex = "9999";
|
|
120
|
+
clone.style.pointerEvents = "none";
|
|
121
|
+
document.body.appendChild(clone);
|
|
122
|
+
return clone;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _moveTouchClone(clone, x, y) {
|
|
126
|
+
if (!clone) return;
|
|
127
|
+
const w = parseFloat(clone.style.width) || 0;
|
|
128
|
+
clone.style.left = (x - w / 2) + "px";
|
|
129
|
+
clone.style.top = (y - 40) + "px";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _removeTouchClone() {
|
|
133
|
+
if (_touchClone && _touchClone.parentNode) {
|
|
134
|
+
_touchClone.parentNode.removeChild(_touchClone);
|
|
135
|
+
}
|
|
136
|
+
_touchClone = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _columnFromPoint(x, y) {
|
|
140
|
+
const el = document.elementFromPoint(x, y);
|
|
141
|
+
if (!el) return null;
|
|
142
|
+
const colEl = el.closest(".kanban-column");
|
|
143
|
+
if (!colEl) return null;
|
|
144
|
+
for (const col of COLUMNS) {
|
|
145
|
+
if (colEl.getAttribute("data-col") === col.id) return col.id;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function _handleTouchDrop(colId) {
|
|
151
|
+
const taskId = touchDragId.value;
|
|
152
|
+
touchDragId.value = null;
|
|
153
|
+
touchOverCol.value = null;
|
|
154
|
+
if (!taskId || !colId) return;
|
|
155
|
+
|
|
156
|
+
const currentTask = (tasksData.value || []).find((t) => t.id === taskId);
|
|
157
|
+
if (!currentTask) return;
|
|
158
|
+
const currentCol = getColumnForStatus(currentTask.status);
|
|
159
|
+
if (currentCol === colId) return;
|
|
160
|
+
|
|
161
|
+
const newStatus = COLUMN_TO_STATUS[colId] || "todo";
|
|
162
|
+
const col = COLUMNS.find((c) => c.id === colId);
|
|
163
|
+
haptic("medium");
|
|
164
|
+
|
|
165
|
+
const prev = cloneValue(tasksData.value);
|
|
166
|
+
try {
|
|
167
|
+
await runOptimistic(
|
|
168
|
+
() => {
|
|
169
|
+
tasksData.value = tasksData.value.map((t) =>
|
|
170
|
+
t.id === taskId ? { ...t, status: newStatus } : t,
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
async () => {
|
|
174
|
+
const res = await apiFetch("/api/tasks/update", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: JSON.stringify({ taskId, status: newStatus }),
|
|
177
|
+
});
|
|
178
|
+
if (res?.data) {
|
|
179
|
+
tasksData.value = tasksData.value.map((t) =>
|
|
180
|
+
t.id === taskId ? { ...t, ...res.data } : t,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return res;
|
|
184
|
+
},
|
|
185
|
+
() => {
|
|
186
|
+
tasksData.value = prev;
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
showToast(`Moved to ${col ? col.title : colId}`, "success");
|
|
190
|
+
} catch {
|
|
191
|
+
/* toast via apiFetch */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ─── Inline create for a column ─── */
|
|
196
|
+
async function createTaskInColumn(columnStatus, title) {
|
|
197
|
+
haptic("medium");
|
|
198
|
+
try {
|
|
199
|
+
await apiFetch("/api/tasks/create", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
title,
|
|
203
|
+
status: columnStatus,
|
|
204
|
+
draft: columnStatus === "draft",
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
showToast("Task created", "success");
|
|
208
|
+
await loadTasks();
|
|
209
|
+
} catch {
|
|
210
|
+
/* toast via apiFetch */
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ─── KanbanCard ─── */
|
|
215
|
+
function KanbanCard({ task, onOpen }) {
|
|
216
|
+
const onDragStart = useCallback((e) => {
|
|
217
|
+
dragTaskId.value = task.id;
|
|
218
|
+
e.dataTransfer.effectAllowed = "move";
|
|
219
|
+
e.dataTransfer.setData("text/plain", task.id);
|
|
220
|
+
e.currentTarget.classList.add("dragging");
|
|
221
|
+
}, [task.id]);
|
|
222
|
+
|
|
223
|
+
const onDragEnd = useCallback((e) => {
|
|
224
|
+
dragTaskId.value = null;
|
|
225
|
+
e.currentTarget.classList.remove("dragging");
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
/* ─ Touch drag handlers ─ */
|
|
229
|
+
const onTouchStart = useCallback((e) => {
|
|
230
|
+
const touch = e.touches[0];
|
|
231
|
+
_touchStartX = touch.clientX;
|
|
232
|
+
_touchStartY = touch.clientY;
|
|
233
|
+
_touchMoved = false;
|
|
234
|
+
touchDragId.value = task.id;
|
|
235
|
+
}, [task.id]);
|
|
236
|
+
|
|
237
|
+
const onTouchMove = useCallback((e) => {
|
|
238
|
+
const touch = e.touches[0];
|
|
239
|
+
const dx = touch.clientX - _touchStartX;
|
|
240
|
+
const dy = touch.clientY - _touchStartY;
|
|
241
|
+
|
|
242
|
+
// Only start drag after a small threshold to distinguish from scroll
|
|
243
|
+
if (!_touchMoved && Math.abs(dx) < 10 && Math.abs(dy) < 10) return;
|
|
244
|
+
|
|
245
|
+
if (!_touchMoved) {
|
|
246
|
+
_touchMoved = true;
|
|
247
|
+
_removeTouchClone();
|
|
248
|
+
_touchClone = _createTouchClone(e.currentTarget);
|
|
249
|
+
haptic("medium");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
e.preventDefault(); // prevent scroll during drag
|
|
253
|
+
_moveTouchClone(_touchClone, touch.clientX, touch.clientY);
|
|
254
|
+
|
|
255
|
+
const colId = _columnFromPoint(touch.clientX, touch.clientY);
|
|
256
|
+
touchOverCol.value = colId;
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
const onTouchEnd = useCallback(() => {
|
|
260
|
+
const colId = touchOverCol.value;
|
|
261
|
+
_removeTouchClone();
|
|
262
|
+
if (_touchMoved && colId) {
|
|
263
|
+
_handleTouchDrop(colId);
|
|
264
|
+
} else {
|
|
265
|
+
touchDragId.value = null;
|
|
266
|
+
touchOverCol.value = null;
|
|
267
|
+
}
|
|
268
|
+
_touchMoved = false;
|
|
269
|
+
}, []);
|
|
270
|
+
|
|
271
|
+
const onTouchCancel = useCallback(() => {
|
|
272
|
+
_removeTouchClone();
|
|
273
|
+
touchDragId.value = null;
|
|
274
|
+
touchOverCol.value = null;
|
|
275
|
+
_touchMoved = false;
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
const priorityColor = PRIORITY_COLORS[task.priority] || null;
|
|
279
|
+
const priorityLabel = PRIORITY_LABELS[task.priority] || null;
|
|
280
|
+
const tags = getTaskTags(task);
|
|
281
|
+
const baseBranch = getTaskBaseBranch(task);
|
|
282
|
+
|
|
283
|
+
return html`
|
|
284
|
+
<div
|
|
285
|
+
class="kanban-card ${dragTaskId.value === task.id ? 'dragging' : ''} ${touchDragId.value === task.id && _touchMoved ? 'dragging' : ''}"
|
|
286
|
+
draggable="true"
|
|
287
|
+
onDragStart=${onDragStart}
|
|
288
|
+
onDragEnd=${onDragEnd}
|
|
289
|
+
onTouchStart=${onTouchStart}
|
|
290
|
+
onTouchMove=${onTouchMove}
|
|
291
|
+
onTouchEnd=${onTouchEnd}
|
|
292
|
+
onTouchCancel=${onTouchCancel}
|
|
293
|
+
onClick=${() => onOpen(task.id)}
|
|
294
|
+
>
|
|
295
|
+
${priorityLabel && html`
|
|
296
|
+
<span class="kanban-card-badge" style="background:${priorityColor}">${priorityLabel}</span>
|
|
297
|
+
`}
|
|
298
|
+
<div class="kanban-card-title">${truncate(task.title || "(untitled)", 80)}</div>
|
|
299
|
+
${task.description && html`
|
|
300
|
+
<div class="kanban-card-desc">${truncate(task.description, 72)}</div>
|
|
301
|
+
`}
|
|
302
|
+
${baseBranch && html`
|
|
303
|
+
<div class="kanban-card-base">Base: <code>${truncate(baseBranch, 24)}</code></div>
|
|
304
|
+
`}
|
|
305
|
+
${tags.length > 0 && html`
|
|
306
|
+
<div class="kanban-card-tags">
|
|
307
|
+
${tags.map((tag) => html`<span class="tag-chip">#${tag}</span>`)}
|
|
308
|
+
</div>
|
|
309
|
+
`}
|
|
310
|
+
<div class="kanban-card-meta">
|
|
311
|
+
<span class="kanban-card-id">${typeof task.id === "string" ? truncate(task.id, 12) : task.id}</span>
|
|
312
|
+
${task.created_at && html`<span>${formatRelative(task.created_at)}</span>`}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ─── KanbanColumn ─── */
|
|
319
|
+
function KanbanColumn({ col, tasks, onOpen }) {
|
|
320
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
321
|
+
const inputRef = useRef(null);
|
|
322
|
+
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (showCreate && inputRef.current) inputRef.current.focus();
|
|
325
|
+
}, [showCreate]);
|
|
326
|
+
|
|
327
|
+
const onDragOver = useCallback((e) => {
|
|
328
|
+
e.preventDefault();
|
|
329
|
+
e.dataTransfer.dropEffect = "move";
|
|
330
|
+
dragOverCol.value = col.id;
|
|
331
|
+
}, [col.id]);
|
|
332
|
+
|
|
333
|
+
const onDragLeave = useCallback(() => {
|
|
334
|
+
if (dragOverCol.value === col.id) dragOverCol.value = null;
|
|
335
|
+
}, [col.id]);
|
|
336
|
+
|
|
337
|
+
const onDrop = useCallback(async (e) => {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
dragOverCol.value = null;
|
|
340
|
+
const taskId = e.dataTransfer.getData("text/plain") || dragTaskId.value;
|
|
341
|
+
dragTaskId.value = null;
|
|
342
|
+
if (!taskId) return;
|
|
343
|
+
|
|
344
|
+
const currentTask = (tasksData.value || []).find((t) => t.id === taskId);
|
|
345
|
+
if (!currentTask) return;
|
|
346
|
+
const currentCol = getColumnForStatus(currentTask.status);
|
|
347
|
+
if (currentCol === col.id) return;
|
|
348
|
+
|
|
349
|
+
const newStatus = COLUMN_TO_STATUS[col.id] || "todo";
|
|
350
|
+
haptic("medium");
|
|
351
|
+
|
|
352
|
+
const prev = cloneValue(tasksData.value);
|
|
353
|
+
try {
|
|
354
|
+
await runOptimistic(
|
|
355
|
+
() => {
|
|
356
|
+
tasksData.value = tasksData.value.map((t) =>
|
|
357
|
+
t.id === taskId ? { ...t, status: newStatus } : t,
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
async () => {
|
|
361
|
+
const res = await apiFetch("/api/tasks/update", {
|
|
362
|
+
method: "POST",
|
|
363
|
+
body: JSON.stringify({ taskId, status: newStatus }),
|
|
364
|
+
});
|
|
365
|
+
if (res?.data) {
|
|
366
|
+
tasksData.value = tasksData.value.map((t) =>
|
|
367
|
+
t.id === taskId ? { ...t, ...res.data } : t,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return res;
|
|
371
|
+
},
|
|
372
|
+
() => {
|
|
373
|
+
tasksData.value = prev;
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
showToast(`Moved to ${col.title}`, "success");
|
|
377
|
+
} catch {
|
|
378
|
+
/* toast via apiFetch */
|
|
379
|
+
}
|
|
380
|
+
}, [col.id, col.title]);
|
|
381
|
+
|
|
382
|
+
const handleInlineKeyDown = useCallback((e) => {
|
|
383
|
+
if (e.key === "Enter" && e.target.value.trim()) {
|
|
384
|
+
createTaskInColumn(COLUMN_TO_STATUS[col.id] || "todo", e.target.value.trim());
|
|
385
|
+
e.target.value = "";
|
|
386
|
+
setShowCreate(false);
|
|
387
|
+
}
|
|
388
|
+
if (e.key === "Escape") {
|
|
389
|
+
setShowCreate(false);
|
|
390
|
+
}
|
|
391
|
+
}, [col.id]);
|
|
392
|
+
|
|
393
|
+
const isOver = dragOverCol.value === col.id || touchOverCol.value === col.id;
|
|
394
|
+
|
|
395
|
+
return html`
|
|
396
|
+
<div
|
|
397
|
+
class="kanban-column ${isOver ? 'drag-over' : ''}"
|
|
398
|
+
data-col=${col.id}
|
|
399
|
+
onDragOver=${onDragOver}
|
|
400
|
+
onDragLeave=${onDragLeave}
|
|
401
|
+
onDrop=${onDrop}
|
|
402
|
+
>
|
|
403
|
+
<div class="kanban-column-header" style="border-bottom-color: ${col.color}">
|
|
404
|
+
<span>${col.icon}</span>
|
|
405
|
+
<span class="kanban-column-title">${col.title}</span>
|
|
406
|
+
<span class="kanban-count">${tasks.length}</span>
|
|
407
|
+
<button
|
|
408
|
+
class="kanban-add-btn"
|
|
409
|
+
onClick=${() => { setShowCreate(!showCreate); haptic(); }}
|
|
410
|
+
title="Add task to ${col.title}"
|
|
411
|
+
>+</button>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="kanban-cards">
|
|
414
|
+
${showCreate && html`
|
|
415
|
+
<input
|
|
416
|
+
ref=${inputRef}
|
|
417
|
+
class="kanban-inline-create"
|
|
418
|
+
placeholder="Task title…"
|
|
419
|
+
onKeyDown=${handleInlineKeyDown}
|
|
420
|
+
onBlur=${() => setShowCreate(false)}
|
|
421
|
+
/>
|
|
422
|
+
`}
|
|
423
|
+
${tasks.length
|
|
424
|
+
? tasks.map((task) => html`
|
|
425
|
+
<${KanbanCard} key=${task.id} task=${task} onOpen=${onOpen} />
|
|
426
|
+
`)
|
|
427
|
+
: html`<div class="kanban-empty-col">Drop tasks here</div>`
|
|
428
|
+
}
|
|
429
|
+
</div>
|
|
430
|
+
<div class="kanban-scroll-fade"></div>
|
|
431
|
+
</div>
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* ─── KanbanBoard (main export) ─── */
|
|
436
|
+
export function KanbanBoard({ onOpenTask }) {
|
|
437
|
+
const cols = columnData.value;
|
|
438
|
+
|
|
439
|
+
return html`
|
|
440
|
+
<div class="kanban-board">
|
|
441
|
+
${COLUMNS.map((col) => html`
|
|
442
|
+
<${KanbanColumn}
|
|
443
|
+
key=${col.id}
|
|
444
|
+
col=${col}
|
|
445
|
+
tasks=${cols[col.id] || []}
|
|
446
|
+
onOpen=${onOpenTask}
|
|
447
|
+
/>
|
|
448
|
+
`)}
|
|
449
|
+
</div>
|
|
450
|
+
`;
|
|
451
|
+
}
|