From 957907c87adf0c3f5a8bd8e010192f4b0967027c Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Thu, 19 Feb 2026 22:57:10 -0600 Subject: [PATCH] Refactor tasks to label workflow and simplify board layout --- data/tasks.json | 46 ++- src/app/api/tasks/route.ts | 19 +- src/app/page.tsx | 589 +++++++++++++++++++++---------------- 3 files changed, 390 insertions(+), 264 deletions(-) diff --git a/data/tasks.json b/data/tasks.json index 823d444..0e8efb0 100644 --- a/data/tasks.json +++ b/data/tasks.json @@ -50,7 +50,8 @@ ], "tags": [ "ui", - "rewrite" + "rewrite", + "Web Projects" ] }, { @@ -64,7 +65,8 @@ "comments": [], "tags": [ "ios", - "social" + "social", + "OpenClaw iOS" ], "createdAt": "2026-02-18T17:01:23.109Z", "updatedAt": "2026-02-20T02:28:23.700Z" @@ -98,7 +100,8 @@ "gitea", "git", "automation", - "infrastructure" + "infrastructure", + "Web Projects" ] }, { @@ -125,7 +128,8 @@ "ux", "redesign", "dashboard", - "monitoring" + "monitoring", + "Web Projects" ] }, { @@ -157,7 +161,8 @@ "blog", "ui", "markdown", - "links" + "links", + "Web Projects" ] }, { @@ -184,7 +189,8 @@ "cron", "bug", "infrastructure", - "urgent" + "urgent", + "Web Projects" ] }, { @@ -210,7 +216,8 @@ "debugging", "research", "infrastructure", - "root-cause" + "root-cause", + "Web Projects" ] }, { @@ -242,7 +249,8 @@ "ui", "sync", "localstorage", - "real-time" + "real-time", + "Web Projects" ] }, { @@ -268,7 +276,8 @@ "ui", "kanban", "feature", - "priority" + "priority", + "Web Projects" ] }, { @@ -301,7 +310,8 @@ "screenshot", "macos", "openclaw", - "investigation" + "investigation", + "Web Projects" ] }, { @@ -366,7 +376,8 @@ "blog", "ui", "markdown", - "frontend" + "frontend", + "Web Projects" ] }, { @@ -400,7 +411,8 @@ "podcast", "audio", "digest", - "accessibility" + "accessibility", + "Web Projects" ] }, { @@ -444,7 +456,8 @@ "backup", "infrastructure", "data-persistence", - "automation" + "automation", + "Web Projects" ] }, { @@ -453,7 +466,10 @@ "title": "Add Sprint functionality to Gantt Board", "projectId": "2", "sprintId": "sprint-1", - "updatedAt": "2026-02-20T01:52:57.259Z" + "updatedAt": "2026-02-20T01:52:57.259Z", + "tags": [ + "Web Projects" + ] } ], "lastUpdated": 1771556223745, @@ -490,4 +506,4 @@ "createdAt": "2026-02-20T01:37:45.241Z" } ] -} \ No newline at end of file +} diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 08cd911..e98f4e4 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -3,6 +3,8 @@ import { readFileSync, writeFileSync, existsSync } from "fs"; import { join } from "path"; const DATA_FILE = join(process.cwd(), "data", "tasks.json"); +console.log('>>> API ROUTE: module loaded, process.cwd():', process.cwd()); +console.log('>>> API ROUTE: DATA_FILE:', DATA_FILE); interface Task { id: string; @@ -58,13 +60,20 @@ const defaultData: DataStore = { }; function getData(): DataStore { + console.log('>>> getData: checking file:', DATA_FILE); + console.log('>>> getData: exists?', existsSync(DATA_FILE)); if (!existsSync(DATA_FILE)) { + console.log('>>> getData: file not found, returning defaultData'); return defaultData; } try { - const data = readFileSync(DATA_FILE, "utf-8"); - return JSON.parse(data); - } catch { + const rawData = readFileSync(DATA_FILE, "utf-8"); + console.log('>>> getData: read file, length:', rawData.length); + const data = JSON.parse(rawData); + console.log('>>> getData: parsed data has', data.tasks?.length, 'tasks'); + return data; + } catch (err) { + console.error('>>> getData: error reading/parsing file:', err); return defaultData; } } @@ -80,7 +89,11 @@ function saveData(data: DataStore) { // GET - fetch all tasks, projects, and sprints export async function GET() { + console.log('>>> API GET: fetching data'); + console.log('>>> API GET: DATA_FILE path:', DATA_FILE); const data = getData(); + console.log('>>> API GET: returning data with', data.tasks?.length, 'tasks,', data.projects?.length, 'projects,', data.sprints?.length, 'sprints'); + console.log('>>> API GET: lastUpdated:', data.lastUpdated); return NextResponse.json(data); } diff --git a/src/app/page.tsx b/src/app/page.tsx index fa51a40..8bcb1e6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,15 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useMemo } from "react" import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" -import { useTaskStore, Task, TaskType, TaskStatus, Priority, Project } from "@/stores/useTaskStore" +import { useTaskStore, Task, TaskType, TaskStatus, Priority } from "@/stores/useTaskStore" import { BacklogView } from "@/components/BacklogView" -import { Plus, MessageSquare, Calendar, Tag, Trash2, Edit2, X, Check, MoreHorizontal, LayoutGrid, ListTodo } from "lucide-react" +import { Plus, MessageSquare, Calendar, Trash2, Edit2, X, LayoutGrid, ListTodo } from "lucide-react" const typeColors: Record = { idea: "bg-purple-500", @@ -63,23 +63,16 @@ export default function Home() { sprints, selectedProjectId, selectedTaskId, - selectedSprintId, - selectProject, - addProject, - deleteProject, addTask, updateTask, deleteTask, selectTask, addComment, deleteComment, - getTasksByProject, syncFromServer, isLoading, } = useTaskStore() - const [newProjectName, setNewProjectName] = useState("") - const [showNewProject, setShowNewProject] = useState(false) const [newTaskOpen, setNewTaskOpen] = useState(false) const [newTask, setNewTask] = useState>({ title: "", @@ -90,17 +83,33 @@ export default function Home() { tags: [], }) const [newComment, setNewComment] = useState("") - const [editingTask, setEditingTask] = useState(null) const [viewMode, setViewMode] = useState<'kanban' | 'backlog'>('kanban') const [editedTask, setEditedTask] = useState(null) + const [newTaskLabelInput, setNewTaskLabelInput] = useState("") + const [editedTaskLabelInput, setEditedTaskLabelInput] = useState("") + + const getTags = (taskLike: { tags?: unknown }) => { + if (!Array.isArray(taskLike.tags)) return [] as string[] + return taskLike.tags.filter((tag): tag is string => typeof tag === "string" && tag.trim().length > 0) + } + + const labelUsage = useMemo(() => { + const counts = new Map() + tasks.forEach((task) => { + getTags(task).forEach((label) => { + counts.set(label, (counts.get(label) || 0) + 1) + }) + }) + return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]) + }, [tasks]) + + const allLabels = useMemo(() => labelUsage.map(([label]) => label), [labelUsage]) // Sync from server on mount useEffect(() => { console.log('>>> PAGE: useEffect for syncFromServer running') - console.log('>>> PAGE: tasks count before sync:', tasks.length) syncFromServer().then(() => { console.log('>>> PAGE: syncFromServer completed') - console.log('>>> PAGE: tasks count after sync:', tasks.length) }) }, [syncFromServer]) @@ -109,17 +118,17 @@ export default function Home() { console.log('>>> PAGE: tasks changed, new count:', tasks.length) }, [tasks]) - const selectedProject = projects.find((p) => p.id === selectedProjectId) const selectedTask = tasks.find((t) => t.id === selectedTaskId) + const editedTaskTags = editedTask ? getTags(editedTask) : [] - // Set editedTask when selectedTask changes useEffect(() => { if (selectedTask) { - setEditedTask({ ...selectedTask }) + // eslint-disable-next-line react-hooks/set-state-in-effect + setEditedTask({ ...selectedTask, tags: getTags(selectedTask) }) + setEditedTaskLabelInput("") } }, [selectedTask]) - const projectTasks = selectedProjectId ? getTasksByProject(selectedProjectId) : [] - + // Get current active sprint (across all projects) const now = new Date() const currentSprint = sprints.find((s) => @@ -133,27 +142,38 @@ export default function Home() { ? tasks.filter((t) => t.sprintId === currentSprint.id) : [] - const handleAddProject = () => { - if (newProjectName.trim()) { - addProject(newProjectName.trim()) - setNewProjectName("") - setShowNewProject(false) - } + const toLabel = (raw: string) => raw.trim().replace(/^#/, "") + + const addUniqueLabel = (existing: string[], raw: string) => { + const nextLabel = toLabel(raw) + if (!nextLabel) return existing + const alreadyExists = existing.some((label) => label.toLowerCase() === nextLabel.toLowerCase()) + return alreadyExists ? existing : [...existing, nextLabel] } + const removeLabel = (existing: string[], labelToRemove: string) => + existing.filter((label) => label.toLowerCase() !== labelToRemove.toLowerCase()) + const handleAddTask = () => { if (newTask.title?.trim()) { // If a specific sprint is selected, use that sprint's project const selectedSprint = newTask.sprintId ? sprints.find(s => s.id === newTask.sprintId) : null - const targetProjectId = selectedSprint?.projectId || selectedProjectId || '2' - - addTask({ - ...newTask, + const targetProjectId = selectedSprint?.projectId || selectedProjectId || projects[0]?.id || '2' + + const taskToCreate: Omit = { + title: newTask.title.trim(), + description: newTask.description?.trim() || undefined, + type: (newTask.type || "task") as TaskType, + priority: (newTask.priority || "medium") as Priority, + status: (newTask.status || "backlog") as TaskStatus, + tags: newTask.tags || [], projectId: targetProjectId, - status: newTask.status || "backlog", - sprintId: newTask.sprintId || currentSprint?.id, // Use selected sprint or default to current - } as any) + sprintId: newTask.sprintId || currentSprint?.id, + } + + addTask(taskToCreate) setNewTask({ title: "", description: "", type: "task", priority: "medium", status: "backlog", tags: [], sprintId: undefined }) + setNewTaskLabelInput("") setNewTaskOpen(false) } } @@ -165,10 +185,6 @@ export default function Home() { } } - const handleUpdateTaskStatus = (taskId: string, newStatus: TaskStatus) => { - updateTask(taskId, { status: newStatus }) - } - return (
{/* Header */} @@ -177,7 +193,7 @@ export default function Home() {

- OpenClaw Project Hub + OpenClaw Task Hub

Track ideas, tasks, bugs, and plans — with threaded notes @@ -194,7 +210,7 @@ export default function Home() { )} - {tasks.length} tasks · {projects.length} projects + {tasks.length} tasks · {allLabels.length} labels

@@ -202,122 +218,70 @@ export default function Home() {
-
- {/* Sidebar - Sprint Info */} - - - {/* Main Content */} -
- {selectedProject ? ( - <> -
-
-

- {selectedProject.name} -

-

- {sprintTasks.length} tasks · {sprintTasks.filter((t) => t.status === "done").length} done -

-
-
- {/* View Toggle */} -
- - -
- + {currentSprint && Active}
- - {/* View Content */} - {viewMode === 'backlog' ? ( - - ) : ( - <> - {/* Current Sprint Header */} -
-
-
-

Sprint 1

-

Feb 16 - Feb 22, 2026

-
- Active -
-
- {/* Kanban Columns */} + {/* Kanban Columns */}
{sprintColumns.map((column) => { // Filter tasks by column statuses @@ -337,111 +301,96 @@ export default function Home() {
{columnTasks.map((task) => { - const taskProject = projects.find((p) => p.id === task.projectId) + const taskTags = getTags(task) return ( - selectTask(task.id)} - > - -
-
- - {typeLabels[task.type]} - - {taskProject && ( + selectTask(task.id)} + > + +
+
- {taskProject.name} + {typeLabels[task.type]} - )} -
-
- - -
-
- -

{task.title}

- {task.description && ( -

- {task.description} -

- )} - -
-
- - {task.priority} - - {task.comments && task.comments.length > 0 && ( - - - {task.comments.length} - - )} -
- {task.dueDate && ( - - - {new Date(task.dueDate).toLocaleDateString()} - - )} -
- - {task.tags.length > 0 && ( -
- {task.tags.map((tag) => ( - +
+ + +
- )} -
-
- )})} + +

{task.title}

+ {task.description && ( +

+ {task.description} +

+ )} + +
+
+ + {task.priority} + + {task.comments && task.comments.length > 0 && ( + + + {task.comments.length} + + )} +
+ {task.dueDate && ( + + + {new Date(task.dueDate).toLocaleDateString()} + + )} +
+ + {taskTags.length > 0 && ( +
+ {taskTags.map((tag) => ( + + {tag} + + ))} +
+ )} + + + ) + })}
) })}
- - )} - ) : ( -
-

Select a project to view tasks

-
)} -
-
+
{/* New Task Dialog */} @@ -528,13 +477,83 @@ export default function Home() {
- - setNewTask({ ...newTask, tags: e.target.value.split(",").map((t) => t.trim()).filter(Boolean) })} - className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" - placeholder="ios, backend, urgent" - /> + +
+ {newTask.tags && newTask.tags.length > 0 && ( +
+ {newTask.tags.map((label) => ( + + {label} + + + ))} +
+ )} +
+ setNewTaskLabelInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault() + setNewTask((prev) => ({ + ...prev, + tags: addUniqueLabel(prev.tags || [], newTaskLabelInput), + })) + setNewTaskLabelInput("") + } + }} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" + placeholder="Type a label and press Enter" + /> + +
+ + {allLabels.map((label) => ( + + {allLabels.filter((label) => !(newTask.tags || []).some((tag) => tag.toLowerCase() === label.toLowerCase())).slice(0, 8).length > 0 && ( +
+ {allLabels + .filter((label) => !(newTask.tags || []).some((tag) => tag.toLowerCase() === label.toLowerCase())) + .slice(0, 8) + .map((label) => ( + + ))} +
+ )} +
@@ -640,6 +659,84 @@ export default function Home() { + {/* Labels */} +
+ +
+ {editedTaskTags.length > 0 && ( +
+ {editedTaskTags.map((label) => ( + + {label} + + + ))} +
+ )} +
+ setEditedTaskLabelInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault() + setEditedTask({ ...editedTask, tags: addUniqueLabel(editedTaskTags, editedTaskLabelInput) }) + setEditedTaskLabelInput("") + } + }} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" + placeholder="Type a label and press Enter" + /> + +
+ + {allLabels.map((label) => ( + + {allLabels.filter((label) => !editedTaskTags.some((tag) => tag.toLowerCase() === label.toLowerCase())).slice(0, 8).length > 0 && ( +
+ {allLabels + .filter((label) => !editedTaskTags.some((tag) => tag.toLowerCase() === label.toLowerCase())) + .slice(0, 8) + .map((label) => ( + + ))} +
+ )} +
+
+ {/* Comments Section */}