From 2de8956e5bd8a3cbea6712efdb8bbf61cefa8eab Mon Sep 17 00:00:00 2001 From: paryanineil <35301792+paryanineil@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:28:51 +0530 Subject: [PATCH 1/2] feat(tasks): allow changing a task's project from the detail sheet Adds a Project selector to the task detail sheet so a task can be moved to a different project inline, instead of being fixed once created. When the project changes, the Milestone and Depends On fields are cleared because they reference records scoped to the previous project (the backend does not auto-clear them, which would otherwise leave the task pointing at a milestone / task from a different project). The Milestone and Depends On pickers also now filter by the currently selected project so their options stay consistent after a switch. Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/TaskDetailSheet.tsx | 35 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/TaskDetailSheet.tsx b/frontend/src/components/TaskDetailSheet.tsx index 604bfa9..dce6c8e 100644 --- a/frontend/src/components/TaskDetailSheet.tsx +++ b/frontend/src/components/TaskDetailSheet.tsx @@ -92,6 +92,7 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient const [status, setStatus] = useState("Backlog") const [priority, setPriority] = useState("Medium") const [size, setSize] = useState("") + const [project, setProject] = useState("") const [milestone, setMilestone] = useState("") const [dependsOn, setDependsOn] = useState("") const [prLink, setPrLink] = useState("") @@ -181,8 +182,8 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient }) }, [allMembers, user?.email]) - const milestoneFilters = useMemo(() => ({ project: task?.project }), [task?.project]) - const dependsOnFilters = useMemo(() => ({ project: task?.project, is_archived: 0 }), [task?.project]) + const milestoneFilters = useMemo(() => ({ project }), [project]) + const dependsOnFilters = useMemo(() => ({ project, is_archived: 0 }), [project]) const [assigneePopoverOpen, setAssigneePopoverOpen] = useState(false) @@ -197,6 +198,7 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient setStatus(task.status) setPriority(task.priority) setSize(task.size || "") + setProject(task.project || "") setMilestone(task.milestone || "") setDependsOn(task.depends_on || "") setPrLink(task.pr_link || "") @@ -258,7 +260,7 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient autosaveTimerRef.current = undefined } } - }, [title, description, status, priority, size, milestone, dependsOn, prLink, dueDate, startDate, completedOn, recurrenceFrequency, recurrenceEndDate, open, isClient]) + }, [title, description, status, priority, size, project, milestone, dependsOn, prLink, dueDate, startDate, completedOn, recurrenceFrequency, recurrenceEndDate, open, isClient]) const assignedMemberNames = useMemo(() => new Set(assignees.map((a) => a.member)), [assignees]) @@ -282,6 +284,16 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient } } + const handleProjectChange = (newProject: string) => { + if (!newProject || newProject === project) return + setProject(newProject) + // Milestone and Depends On belong to the previous project — clear them so + // the task isn't left pointing at records from a different project. + setMilestone("") + setDependsOn("") + markEdited() + } + const handleSave = async (silent = false) => { if (saving || !title.trim()) return if (autosaveTimerRef.current) { @@ -297,6 +309,7 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient status, priority, size: size || null, + project, milestone: milestone || null, depends_on: dependsOn || null, pr_link: prLink || null, @@ -512,6 +525,22 @@ export function TaskDetailSheet({ task, open, onOpenChange, onUpdated, hasClient + {/* Project */} +
+ + {isClient ? ( +

{project || "None"}

+ ) : ( + + )} +
+ {/* Milestone */}
From 3ae388e6085d02713b7c42ce221f0c4b23da1599 Mon Sep 17 00:00:00 2001 From: paryanineil <35301792+paryanineil@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:56:12 +0530 Subject: [PATCH 2/2] test(lock-due-date): target the Due Date field by exact label The switch-project change adds a Project field that surfaces the project's title in the detail sheet. The lock-due-date spec located the Due Date field with `.grid.gap-2` filtered by `hasText: "Due Date"`, which now also matches the Project field when a project's name contains "Due Date" (e.g. the test fixture "E2E Lock Due Date"), causing a strict-mode violation. Filter by an exact-text "Due Date" label instead, so the locator targets only the Due Date field regardless of other fields' displayed content. Co-Authored-By: Claude Opus 4.8 --- e2e/tests/lock-due-date.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/tests/lock-due-date.spec.ts b/e2e/tests/lock-due-date.spec.ts index b7f6959..f927f67 100644 --- a/e2e/tests/lock-due-date.spec.ts +++ b/e2e/tests/lock-due-date.spec.ts @@ -167,7 +167,7 @@ test.describe("Lock Due Date Config", () => { const sheet = await openTaskSheet(page, pastDueTask.title); // Due date should be rendered as plain text (not a button/DatePicker) - const dueDateLabel = sheet.locator(".grid.gap-2").filter({ hasText: "Due Date" }); + const dueDateLabel = sheet.locator(".grid.gap-2").filter({ has: page.getByText("Due Date", { exact: true }) }); const lockedText = dueDateLabel.locator("p.text-sm.text-muted-foreground"); await expect(lockedText).toBeVisible(); await expect(lockedText).toHaveAttribute( @@ -187,7 +187,7 @@ test.describe("Lock Due Date Config", () => { const sheet = await openTaskSheet(page, futureDueTask.title); // Due date should be rendered as a DatePicker (button) - const dueDateLabel = sheet.locator(".grid.gap-2").filter({ hasText: "Due Date" }); + const dueDateLabel = sheet.locator(".grid.gap-2").filter({ has: page.getByText("Due Date", { exact: true }) }); // Should NOT have the locked text with title attribute const lockedText = dueDateLabel.locator('p[title="Due date is locked on or after the due date"]'); @@ -211,7 +211,7 @@ test.describe("Lock Due Date Config", () => { const sheet = await openTaskSheet(page, pastDueTask.title); // Even though due date is in the past, it should be editable because the setting is OFF - const dueDateLabel = sheet.locator(".grid.gap-2").filter({ hasText: "Due Date" }); + const dueDateLabel = sheet.locator(".grid.gap-2").filter({ has: page.getByText("Due Date", { exact: true }) }); // Should NOT have the locked title const lockedText = dueDateLabel.locator('p[title="Due date is locked on or after the due date"]');