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.
This commit is contained in:
npal 2026-09-21 20:04:33 -05:00
parent cb6fd48916
commit 86e43b66f4
3 changed files with 48 additions and 2 deletions

View file

@ -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({
/>
</Stack>
<TaskTemplateItemsField control={control} />
<TaskTemplateItemsField key={String(selectedId)} control={control} />
<Stack direction="row" spacing={2} sx={{ justifyContent: "flex-end" }}>
{!isNew && (

View file

@ -53,6 +53,7 @@ export default function TaskTemplatesPage() {
isNew={isNew}
isSelected={selectedId !== null}
isSaving={isSaving}
selectedId={selectedId}
onDelete={requestDelete}
onSubmit={submitForm}
/>

View file

@ -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"));