feat: implement PL-7, PL-8, PL-9 — state-driven board, module/cycle views, link panels + graph edges

PL-7 — Task Board Columns from States:
- Add State, Module, Cycle, Link TypeScript types to types/index.ts
- Add stateId, moduleId, cycleId, trackedMinutes to Task type
- Rewrite tasks.tsx: fetch states from API, render 5 state-group columns
  (backlog/unstarted/started/completed/cancelled), drag-and-drop updates
  stateId via PATCH /tasks/:id, colored state badges, state filter dropdown,
  project filter
- Fix deprecated POST /tasks/:id/status → PATCH /tasks/:id with stateId
- Update tasks/.tsx: state selector dropdown replaces hardcoded status enum,
  toggle complete uses state-based approach, dependencies replaced with
  link-based UI using /api/links

PL-8 — Module + Cycle Views:
- Create apps/api/src/routes/cycles.ts: full CRUD + task assignment/removal
- Create apps/api/src/routes/links.ts: list/create/delete links between entities
- Register cycleRoutes and linkRoutes in API index
- Add Modules tab to project detail: list modules, expand to show tasks,
  add/remove tasks from modules, create/edit/delete module dialogs
- Add Cycles tab to project detail: sprint board grid, backlog lane,
  manual task transfer between cycles and backlog, create/edit cycle dialogs
- Fix ProjectTasks toggle to use PATCH with stateId instead of deprecated endpoint

PL-9 — Link Panels + Graph:
- Update graph API to read links bidirectionally (source OR target)
- Add link type color map (LINK_TYPE_COLORS) for edge rendering
- Graph edges now colored by linkType (blocks=red, relates=gray, etc.)
- Filter panel shows link types with color indicators
- Task detail Dependencies tab now uses /api/links for add/remove links
- Added link type selector (blocks/relates/parent-child/created-from)
This commit is contained in:
2026-09-07 20:24:54 +00:00
parent 14bb1aa1a8
commit c6328c120a
9 changed files with 1525 additions and 228 deletions
+26 -5
View File
@@ -20,7 +20,7 @@ import type { GraphNode, GraphEdge } from "@/lib/types";
import ForceGraph2D from "react-force-graph-2d";
const ENTITY_TYPES = ["task", "habit", "project", "note", "section", "tag", "domain"];
const RELATIONSHIP_TYPES = ["depends_on", "related_to", "part_of", "references", "parent_of", "child_of", "connects_to"];
const RELATIONSHIP_TYPES = ["depends_on", "related_to", "part_of", "references", "parent_of", "child_of", "connects_to", "relates", "blocks", "parent-child", "created-from", "task_project", "task_domain", "habit_domain", "project_domain", "note_domain", "section_project"];
const ENTITY_COLORS: Record<string, string> = {
task: "#3b82f6",
@@ -32,6 +32,20 @@ const ENTITY_COLORS: Record<string, string> = {
domain: "#6366f1",
};
const LINK_TYPE_COLORS: Record<string, string> = {
relates: "#94a3b8",
blocks: "#ef4444",
"parent-child": "#8b5cf6",
"created-from": "#10b981",
depends_on: "#ef4444",
related_to: "#94a3b8",
part_of: "#8b5cf6",
references: "#f59e0b",
parent_of: "#8b5cf6",
child_of: "#10b981",
connects_to: "#3b82f6",
};
// Graph node types that have a detail page. section/tag/domain nodes appear in
// the graph but have no detail route, so they are intentionally absent.
const NODE_TYPE_ROUTES: Record<string, string> = {
@@ -265,11 +279,17 @@ function GraphPage() {
const isHighlighted = highlightLinks.size === 0 || highlightLinks.has(`${link.source.id}-${link.target.id}`);
const width = isHighlighted ? 1.5 / globalScale : 0.5 / globalScale;
const opacity = isHighlighted ? 0.6 : 0.1;
const linkType = link.type || "relates";
const baseColor = LINK_TYPE_COLORS[linkType] || "#94a3b8";
const r = parseInt(baseColor.slice(1, 3), 16);
const g = parseInt(baseColor.slice(3, 5), 16);
const b = parseInt(baseColor.slice(5, 7), 16);
ctx.beginPath();
ctx.moveTo(link.source.x, link.source.y);
ctx.lineTo(link.target.x, link.target.y);
ctx.strokeStyle = `rgba(148, 163, 184, ${opacity})`;
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`;
ctx.lineWidth = width;
ctx.stroke();
@@ -289,7 +309,7 @@ function GraphPage() {
ctx.lineTo(midX - ux * arrowSize + uy * arrowSize * 0.5, midY - uy * arrowSize - ux * arrowSize * 0.5);
ctx.lineTo(midX - ux * arrowSize - uy * arrowSize * 0.5, midY - uy * arrowSize + ux * arrowSize * 0.5);
ctx.closePath();
ctx.fillStyle = `rgba(148, 163, 184, ${opacity})`;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`;
ctx.fill();
}
}
@@ -425,8 +445,9 @@ function GraphPage() {
checked={enabledRelationships.has(type)}
onCheckedChange={() => toggleRelationship(type)}
/>
<Label htmlFor={"rel-" + type} className="text-sm cursor-pointer">
{type.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
<Label htmlFor={"rel-" + type} className="flex items-center gap-2 text-sm cursor-pointer">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: LINK_TYPE_COLORS[type] || "#94a3b8" }} />
{type.replace(/_/g, " ").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
</Label>
</div>
))}