116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
|
|
import { useState, useEffect, useCallback } from "react";
|
||
|
|
import { useNavigate } from "react-router-dom";
|
||
|
|
import { useVault } from "../App";
|
||
|
|
import { listTasks, toggleTask, type TaskItem } from "../lib/commands";
|
||
|
|
|
||
|
|
type Column = "todo" | "in-progress" | "done";
|
||
|
|
const COLUMNS: { id: Column; label: string; icon: string }[] = [
|
||
|
|
{ id: "todo", label: "To Do", icon: "☐" },
|
||
|
|
{ id: "in-progress", label: "In Progress", icon: "◐" },
|
||
|
|
{ id: "done", label: "Done", icon: "✓" },
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* KanbanView — Visual board extracted from `- [ ]` / `- [/]` / `- [x]` items across vault.
|
||
|
|
*/
|
||
|
|
export function KanbanView() {
|
||
|
|
const { vaultPath, refreshNotes } = useVault();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const [tasks, setTasks] = useState<TaskItem[]>([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [dragItem, setDragItem] = useState<TaskItem | null>(null);
|
||
|
|
|
||
|
|
const loadTasks = useCallback(async () => {
|
||
|
|
if (!vaultPath) return;
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const items = await listTasks(vaultPath);
|
||
|
|
setTasks(items);
|
||
|
|
} catch {
|
||
|
|
setTasks([]);
|
||
|
|
}
|
||
|
|
setLoading(false);
|
||
|
|
}, [vaultPath]);
|
||
|
|
|
||
|
|
useEffect(() => { loadTasks(); }, [loadTasks]);
|
||
|
|
|
||
|
|
const handleDrop = async (column: Column) => {
|
||
|
|
if (!dragItem || dragItem.state === column) return;
|
||
|
|
try {
|
||
|
|
await toggleTask(vaultPath, dragItem.source_path, dragItem.line_number, column);
|
||
|
|
setTasks(prev => prev.map(t =>
|
||
|
|
t.source_path === dragItem.source_path && t.line_number === dragItem.line_number
|
||
|
|
? { ...t, state: column }
|
||
|
|
: t
|
||
|
|
));
|
||
|
|
refreshNotes();
|
||
|
|
} catch (e) {
|
||
|
|
console.error("Toggle failed:", e);
|
||
|
|
}
|
||
|
|
setDragItem(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
const openSource = (task: TaskItem) => {
|
||
|
|
navigate(`/note/${encodeURIComponent(task.source_path)}`);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="kanban-view">
|
||
|
|
<div className="kanban-loading">Loading tasks…</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="kanban-view">
|
||
|
|
<div className="kanban-header">
|
||
|
|
<h2 className="kanban-title">📋 Task Board</h2>
|
||
|
|
<span className="kanban-count">{tasks.length} tasks</span>
|
||
|
|
<button className="kanban-refresh" onClick={loadTasks}>↻</button>
|
||
|
|
</div>
|
||
|
|
<div className="kanban-columns">
|
||
|
|
{COLUMNS.map(col => {
|
||
|
|
const colTasks = tasks.filter(t => t.state === col.id);
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={col.id}
|
||
|
|
className={`kanban-column ${dragItem ? "drop-ready" : ""}`}
|
||
|
|
onDragOver={e => e.preventDefault()}
|
||
|
|
onDrop={() => handleDrop(col.id)}
|
||
|
|
>
|
||
|
|
<div className="kanban-column-header">
|
||
|
|
<span className="kanban-column-icon">{col.icon}</span>
|
||
|
|
<span className="kanban-column-label">{col.label}</span>
|
||
|
|
<span className="badge badge-muted">{colTasks.length}</span>
|
||
|
|
</div>
|
||
|
|
<div className="kanban-column-body">
|
||
|
|
{colTasks.map((task, i) => (
|
||
|
|
<div
|
||
|
|
key={`${task.source_path}-${task.line_number}-${i}`}
|
||
|
|
className="kanban-card"
|
||
|
|
draggable
|
||
|
|
onDragStart={() => setDragItem(task)}
|
||
|
|
onDragEnd={() => setDragItem(null)}
|
||
|
|
>
|
||
|
|
<div className="kanban-card-text">{task.text}</div>
|
||
|
|
<button
|
||
|
|
className="kanban-card-source"
|
||
|
|
onClick={() => openSource(task)}
|
||
|
|
>
|
||
|
|
{task.source_path.replace(".md", "")}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{colTasks.length === 0 && (
|
||
|
|
<div className="kanban-empty">No tasks</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|