shoc-frontend-new/src/domain/settings/task-templates/use-cases/use-task-template-editor.ts
npal 62e5d46b0f fix: address PR #224 review findings and extract task-template copy (SH-378)
Three real behavior deltas flagged in review, each closing a gap
against the "no behavior change" claim:

- use-vendor-portal-session.ts: add retry:false and
  meta:{suppressErrorToast:true} so an invalid/expired token fails
  fast with a single request and no duplicate global toast (this was
  also the root cause of the failing e2e test - the toast and the
  inline error message both had role="alert", tripping a strict-mode
  locator match)
- task-template-schema.ts: trim the name before validating so a
  whitespace-only name is rejected, matching the old manual
  form.name.trim() check
- task-template-detail-form.tsx: block Enter-triggered implicit
  submission on the Template Name / Description inputs, since the
  original page had no <form> element and Enter did nothing
- task-template-items-field.tsx: surface a visible error when a
  loaded item has empty text, since the schema already blocked save
  in that case but gave no way to see why; use-task-template-editor.ts
  now eagerly validates after loading a template so this reflects
  reality immediately instead of only after an unrelated edit

Also extract every user-facing string in the task-templates feature
into TASK_TEMPLATE_COPY (task-template-constants.ts) instead of
inline literals scattered across 5 files.

6 new regression tests cover all of the above; full suite (36 tests
across the 2 refactored areas) still green.
2026-09-18 10:46:16 -05:00

119 lines
3.7 KiB
TypeScript

import { useState } from "react";
import { useForm, useWatch, type UseFormReturn } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import type { UseQueryResult } from "@tanstack/react-query";
import {
EMPTY_TASK_TEMPLATE_FORM,
NEW_TEMPLATE_ID,
} from "@/domain/settings/task-templates/constants/task-template-constants";
import { mapTaskTemplateToFormValues } from "@/domain/settings/task-templates/mappers/task-template-mapper";
import { taskTemplateSchema } from "@/domain/settings/task-templates/schemas/task-template-schema";
import type {
SelectedTaskTemplateId,
TaskTemplate,
TaskTemplateFormValues,
} from "@/domain/settings/task-templates/types/task-template";
import {
useCreateTaskTemplate,
useDeleteTaskTemplate,
useUpdateTaskTemplate,
} from "@/domain/settings/task-templates/use-cases/use-task-template-mutations";
import { useTaskTemplates } from "@/domain/settings/task-templates/use-cases/use-task-templates";
export interface UseTaskTemplateEditorResult {
templatesQuery: UseQueryResult<TaskTemplate[], Error>;
templates: TaskTemplate[];
selectedId: SelectedTaskTemplateId;
isNew: boolean;
isSaving: boolean;
isDeleting: boolean;
form: UseFormReturn<TaskTemplateFormValues>;
deleteConfirmOpen: boolean;
templateName: string;
selectTemplate: (template: TaskTemplate) => void;
startNewTemplate: () => void;
submitForm: () => void;
requestDelete: () => void;
cancelDelete: () => void;
confirmDelete: () => void;
}
export function useTaskTemplateEditor(): UseTaskTemplateEditorResult {
const templatesQuery = useTaskTemplates();
const createTemplate = useCreateTaskTemplate();
const updateTemplate = useUpdateTaskTemplate();
const deleteTemplate = useDeleteTaskTemplate();
const [selectedId, setSelectedId] = useState<SelectedTaskTemplateId>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const form = useForm<TaskTemplateFormValues>({
resolver: zodResolver(taskTemplateSchema),
defaultValues: EMPTY_TASK_TEMPLATE_FORM,
mode: "onChange",
});
const isNew = selectedId === NEW_TEMPLATE_ID;
const templateName = useWatch({ control: form.control, name: "name" });
const selectTemplate = (template: TaskTemplate) => {
setSelectedId(template.id);
form.reset(mapTaskTemplateToFormValues(template));
void form.trigger();
};
const startNewTemplate = () => {
setSelectedId(NEW_TEMPLATE_ID);
form.reset(EMPTY_TASK_TEMPLATE_FORM);
};
const onSubmit = (values: TaskTemplateFormValues) => {
const normalized: TaskTemplateFormValues = {
...values,
items: values.items.map((item, index) => ({ ...item, sortOrder: index })),
};
if (isNew) {
createTemplate.mutate(normalized, {
onSuccess: (result) => setSelectedId(result.id),
});
return;
}
if (selectedId !== null) {
updateTemplate.mutate({ id: selectedId, values: normalized });
}
};
const submitForm = form.handleSubmit(onSubmit);
const requestDelete = () => setDeleteConfirmOpen(true);
const cancelDelete = () => setDeleteConfirmOpen(false);
const confirmDelete = () => {
if (selectedId === null || isNew) return;
deleteTemplate.mutate(selectedId, {
onSuccess: () => {
setSelectedId(null);
form.reset(EMPTY_TASK_TEMPLATE_FORM);
setDeleteConfirmOpen(false);
},
});
};
return {
templatesQuery,
templates: templatesQuery.data ?? [],
selectedId,
isNew,
isSaving: createTemplate.isPending || updateTemplate.isPending,
isDeleting: deleteTemplate.isPending,
form,
deleteConfirmOpen,
templateName,
selectTemplate,
startNewTemplate,
submitForm,
requestDelete,
cancelDelete,
confirmDelete,
};
}