mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 08:33:14 +00:00
fix(completion-docs): keep other rejected values out of the title field
Only the server's TitleRequired rejection is shown on the Document Title field; any other rejected value is reported in a toast. Procedure names and the extra safety note stop at the server's length limits.
This commit is contained in:
parent
0a64f2dec7
commit
6d22d039ef
5 changed files with 49 additions and 8 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { TextField } from "@/components/ui/text-field";
|
||||
import { PROCEDURE_NAME_MAX_LENGTH } from "@/domain/completion-templates/constants/completion-template-constants";
|
||||
import type { CompletionTemplateFormValues } from "@/domain/completion-templates/schemas/completion-template-schema";
|
||||
import { CompletionTemplateAddLink } from "./completion-template-add-link";
|
||||
import { CompletionTemplateRemoveButton } from "./completion-template-remove-button";
|
||||
|
|
@ -25,7 +26,12 @@ function ProcedureFields({ form, index }: ProceduresEditorProps & { index: numbe
|
|||
onBlur={field.onBlur}
|
||||
inputRef={field.ref}
|
||||
placeholder="Procedure name (e.g. Backflow Procedure)"
|
||||
slotProps={{ htmlInput: { "aria-label": `Procedure ${index + 1} name` } }}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
"aria-label": `Procedure ${index + 1} name`,
|
||||
maxLength: PROCEDURE_NAME_MAX_LENGTH,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Controller, useWatch, type UseFormReturn } from "react-hook-form";
|
||||
import { TextField } from "@/components/ui/text-field";
|
||||
import { EXTRA_SAFETY_NOTE_MAX_LENGTH } from "@/domain/completion-templates/constants/completion-template-constants";
|
||||
import type { CompletionTemplateFormValues } from "@/domain/completion-templates/schemas/completion-template-schema";
|
||||
import { CompletionTemplateAddLink } from "./completion-template-add-link";
|
||||
import { CompletionTemplateRemoveButton } from "./completion-template-remove-button";
|
||||
|
|
@ -36,7 +37,12 @@ export function CompletionTemplateSafetyNoteField({ form }: SafetyNoteFieldProps
|
|||
onBlur={field.onBlur}
|
||||
inputRef={field.ref}
|
||||
placeholder="Type safety note…"
|
||||
slotProps={{ htmlInput: { "aria-label": "Extra safety note" } }}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
"aria-label": "Extra safety note",
|
||||
maxLength: EXTRA_SAFETY_NOTE_MAX_LENGTH,
|
||||
},
|
||||
}}
|
||||
sx={{ "& .MuiOutlinedInput-root": { height: "auto" } }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
/** Filled in on every generated document; shown read-only so authors don't retype them. */
|
||||
export const AUTO_POPULATED_FIELDS = ["WO#", "Site Code", "Address", "Service Type"] as const;
|
||||
|
||||
/** Server limits; the inputs stop there instead of failing the save. */
|
||||
export const PROCEDURE_NAME_MAX_LENGTH = 200;
|
||||
export const EXTRA_SAFETY_NOTE_MAX_LENGTH = 2000;
|
||||
|
||||
export const COMPLETION_TEMPLATE_COPY = {
|
||||
pageTitle: "Completion Docs",
|
||||
searchPlaceholder: "Search document title…",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ interface UpdateCompletionTemplateVariables {
|
|||
|
||||
/** The server's blank-title rejection, which the form shows on the title field instead. */
|
||||
export function isTitleRequiredError(error: Error): boolean {
|
||||
return isHTTPError(error) && error.response.status === 422;
|
||||
if (!isHTTPError(error) || error.response.status !== 422) return false;
|
||||
const body: unknown = error.data;
|
||||
return (
|
||||
typeof body === "object" && body !== null && "code" in body && body.code === "TitleRequired"
|
||||
);
|
||||
}
|
||||
|
||||
function invalidateTemplates(queryClient: QueryClient) {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,18 @@ function previewHeadings(dialog: HTMLElement) {
|
|||
.map((heading) => heading.textContent);
|
||||
}
|
||||
|
||||
/** A 422 as the API layer delivers it: ky's parsed body on `data`, the mapped text on `message`. */
|
||||
function unprocessable(body: { code: string; message: string }, message = body.message) {
|
||||
const error = new HTTPError(
|
||||
new Response(null, { status: 422 }),
|
||||
new Request("http://api/workorders/completion-templates"),
|
||||
{} as never,
|
||||
);
|
||||
error.data = body;
|
||||
error.message = message;
|
||||
return error;
|
||||
}
|
||||
|
||||
async function addProcedure(dialog: HTMLElement, index: number, name: string) {
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "+ Add procedure" }));
|
||||
await userEvent.type(within(dialog).getByLabelText(`Procedure ${index} name`), name);
|
||||
|
|
@ -145,11 +157,7 @@ describe("Add Template", () => {
|
|||
|
||||
it("shows the server's blank-title rejection on the title field", async () => {
|
||||
api.apiPost.mockRejectedValue(
|
||||
new HTTPError(
|
||||
new Response(null, { status: 422 }),
|
||||
new Request("http://api/workorders/completion-templates"),
|
||||
{} as never,
|
||||
),
|
||||
unprocessable({ code: "TitleRequired", message: "Document Title is required." }),
|
||||
);
|
||||
const dialog = await openNewTemplate();
|
||||
await userEvent.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), "X");
|
||||
|
|
@ -159,6 +167,19 @@ describe("Add Template", () => {
|
|||
expect(await within(dialog).findByText("Document Title is required")).toBeInTheDocument();
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports any other rejected value in a toast and keeps the form open", async () => {
|
||||
const message = "Procedure name must be 200 characters or fewer.";
|
||||
api.apiPost.mockRejectedValue(unprocessable({ code: "InvalidValue", message }, message));
|
||||
const dialog = await openNewTemplate();
|
||||
await userEvent.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), "X");
|
||||
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: /Add Template/ }));
|
||||
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledWith(message));
|
||||
expect(within(dialog).queryByText("Document Title is required")).toBeNull();
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edit template", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue