9.1 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/tasks/requirements/subtask-reparenting-drag-drop.md | 2026-08-04 | complete |
Implementation Plan: Subtask re-parenting by drag and drop
Overview
The backend reparent contract (PATCH /api/v1/tasks/:id with parent_id, validated by resolveParentID + validateReparentDepth) already exists and is tested. Two deltas make drag-drop "strictly equivalent" to the menu composite (un-nest + nest): (1) normalize inherit_parent workspace mode to shared_group on any effective parent change in the canonical update path, and (2) add the same normalization to the Office dashboard PATCH for parity. The real work is frontend: hoist the sidebar tree's per-level DndContext into one per-group context, add nest drop zones with a pure drop-decision helper, and wire onReparentTask through TaskSwitcher to the existing useNestTask hook. E2E (desktop + mobile touch) comes last.
Backend
Canonical update normalization (apps/backend/internal/task/service/service_tasks.go)
In Service.UpdateTask, the parent block (~line 1225) already resolves and assigns task.ParentID only when the parent effectively changed. After that block, add a small helper normalizeWorkspaceModeAfterReparent(task) that copies task.Metadata["workspace"] and changes only mode: "inherit_parent" → "shared_group" — the exact detach semantics — applied whenever the effective parent changed (set or cleared). The existing full-row persist writes the metadata; the existing parent_id event emission (explicit nil on clear) is unchanged. This covers core PATCH, WS update, the sidebar menu, the Office parent picker (updateTask), and the drag-drop, all through one path.
Office dashboard parity (apps/backend/internal/office/dashboard/service_tasks.go)
DashboardService.UpdateTaskParentID non-empty path currently writes parent_id directly and publishes ["parent_id"]. Change it so that after the repo write it loads the task, and if workspace mode is inherit_parent, persists shared_group and publishes fields ["parent_id", "metadata"]. Add UpdateTaskWorkspaceMode(ctx, taskID, mode) to the office sqlite repository (dialect-aware JSON set, mirroring detachTaskQuery's mode branch) and to the DashboardService repo interface. The empty-parent path keeps routing to the canonical detacher.
Events
No new event types. task.updated already carries parent_id (nil when cleared) and metadata; the normalization rides along. Office OfficeTaskUpdated gains metadata in its fields list only when the mode changed.
Frontend
Drop-decision logic (apps/web/components/task/task-switcher-subtask-dnd.tsx)
- Extract a pure, unit-testable
resolveSidebarDrop({ activeId, overId, groupRootIds, childrenByParent })returning{kind:"nest", parentTaskId}|{kind:"reorder", level:"group"|"subtasks", parentTaskId?, orderedTaskIds}|null.overIdprefixednest:→ nest; otherwise both tasks must share a level (group root list or one parent's children) →arrayMovewithin that level; anything else →null. - Add
NestDropZone(small component):useDroppable({ id: "nest:"+taskId }), absolute left-edge strip,data-testid="nest-drop-zone",data-task-id, hover affordance label "Nest under