Files
kandev/docs/plans/subtask-reparenting-drag-drop/plan.md
T
2026-08-23 17:34:52 +00:00

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. overId prefixed nest: → nest; otherwise both tasks must share a level (group root list or one parent's children) → arrayMove within 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