From 86e43b66f42570427e78e4b6cc8c4fb5410ecc8a Mon Sep 17 00:00:00 2001 From: npal Date: Mon, 21 Sep 2026 20:04:33 -0500 Subject: [PATCH] fix: reset add-item draft on template selection change (SH-378) TaskTemplateItemsField owns newItemText locally but stays mounted across selectTemplate/startNewTemplate (which only call form.reset()), so a typed draft survived switching templates - the base page cleared this draft explicitly in both handleSelect and handleNew. Remount TaskTemplateItemsField on selection change via key={selectedId} instead of lifting the draft into the editor hook: it's transient input-only state, not form data, so this keeps the fix local and lets React's own remount semantics reset it. Confirmed the two new regression tests fail without the key and pass with it. --- .../task-template-detail-form.tsx | 9 ++++- .../(protected)/settings/task-templates.tsx | 1 + .../settings/task-templates.test.tsx | 40 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/app/(protected)/settings/_components/task-templates/task-template-detail-form.tsx b/src/app/(protected)/settings/_components/task-templates/task-template-detail-form.tsx index 889be812..301195ed 100644 --- a/src/app/(protected)/settings/_components/task-templates/task-template-detail-form.tsx +++ b/src/app/(protected)/settings/_components/task-templates/task-template-detail-form.tsx @@ -1,7 +1,10 @@ import type { UseFormReturn } from "react-hook-form"; import { Box, Button, Paper, Stack, TextField, Typography } from "@mui/material"; import { TASK_TEMPLATE_COPY } from "@/domain/settings/task-templates/constants/task-template-constants"; -import type { TaskTemplateFormValues } from "@/domain/settings/task-templates/types/task-template"; +import type { + SelectedTaskTemplateId, + TaskTemplateFormValues, +} from "@/domain/settings/task-templates/types/task-template"; import { TaskTemplateItemsField } from "./task-template-items-field"; function getSubmitLabel(isSaving: boolean, isNew: boolean): string { @@ -14,6 +17,7 @@ interface TaskTemplateDetailFormProps { isNew: boolean; isSelected: boolean; isSaving: boolean; + selectedId: SelectedTaskTemplateId; onDelete: () => void; onSubmit: () => void; } @@ -23,6 +27,7 @@ export function TaskTemplateDetailForm({ isNew, isSelected, isSaving, + selectedId, onDelete, onSubmit, }: TaskTemplateDetailFormProps) { @@ -69,7 +74,7 @@ export function TaskTemplateDetailForm({ /> - + {!isNew && ( diff --git a/src/app/(protected)/settings/task-templates.tsx b/src/app/(protected)/settings/task-templates.tsx index f368d828..bbc98752 100644 --- a/src/app/(protected)/settings/task-templates.tsx +++ b/src/app/(protected)/settings/task-templates.tsx @@ -53,6 +53,7 @@ export default function TaskTemplatesPage() { isNew={isNew} isSelected={selectedId !== null} isSaving={isSaving} + selectedId={selectedId} onDelete={requestDelete} onSubmit={submitForm} /> diff --git a/src/test/app/(protected)/settings/task-templates.test.tsx b/src/test/app/(protected)/settings/task-templates.test.tsx index 785185c8..eb57b26e 100644 --- a/src/test/app/(protected)/settings/task-templates.test.tsx +++ b/src/test/app/(protected)/settings/task-templates.test.tsx @@ -16,6 +16,14 @@ const existingTemplate: TaskTemplate = { ], }; +const secondTemplate: TaskTemplate = { + id: "9", + name: "Plumbing Inspection", + description: "Basic checklist", + isActive: true, + items: [{ itemText: "Check for leaks", sortOrder: 0 }], +}; + afterEach(() => { vi.restoreAllMocks(); }); @@ -88,6 +96,38 @@ describe("TaskTemplatesPage", () => { ]); }); + it("clears the unsaved add-item draft when switching to a different template", async () => { + renderPage([existingTemplate, secondTemplate]); + fireEvent.click(await screen.findByText("HVAC Inspection")); + await screen.findByText("Checklist Items (2)"); + + fireEvent.change(screen.getByPlaceholderText("Add checklist item..."), { + target: { value: "Check filter" }, + }); + expect(screen.getByPlaceholderText("Add checklist item...")).toHaveValue("Check filter"); + + fireEvent.click(screen.getByText("Plumbing Inspection")); + + await screen.findByText("Checklist Items (1)"); + expect(screen.getByPlaceholderText("Add checklist item...")).toHaveValue(""); + }); + + it("clears the unsaved add-item draft when starting a new template", async () => { + renderPage(); + fireEvent.click(await screen.findByText("HVAC Inspection")); + await screen.findByText("Checklist Items (2)"); + + fireEvent.change(screen.getByPlaceholderText("Add checklist item..."), { + target: { value: "Check filter" }, + }); + expect(screen.getByPlaceholderText("Add checklist item...")).toHaveValue("Check filter"); + + fireEvent.click(screen.getByRole("button", { name: "+ New Template" })); + + await screen.findByText("Checklist Items (0)"); + expect(screen.getByPlaceholderText("Add checklist item...")).toHaveValue(""); + }); + it("moves an item up and down without changing the item count", async () => { renderPage(); fireEvent.click(await screen.findByText("HVAC Inspection"));