From 480c72bef45f846b432b28b7c23acf77d1799140 Mon Sep 17 00:00:00 2001 From: smith113-p <205463041+smith113-p@users.noreply.github.com> Date: Fri, 29 May 2026 14:01:34 -0400 Subject: [PATCH 1/3] Block scene submit with pending URL --- frontend/src/components/urlInput/urlInput.tsx | 20 ++++++++++++++++--- .../src/pages/scenes/sceneForm/SceneForm.tsx | 9 +++++++++ .../sceneForm/__tests__/SceneForm.test.tsx | 18 +++++++++++++++++ frontend/src/pages/scenes/sceneForm/schema.ts | 7 +++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/urlInput/urlInput.tsx b/frontend/src/components/urlInput/urlInput.tsx index d5b1d5d3e..195eee251 100644 --- a/frontend/src/components/urlInput/urlInput.tsx +++ b/frontend/src/components/urlInput/urlInput.tsx @@ -31,9 +31,17 @@ interface URLInputProps { lens: Lens; type: ValidSiteTypeEnum; errors?: ErrorsType; + pendingURLError?: string; + onPendingURLChange?: (hasPendingURL: boolean) => void; } -const URLInput: FC = ({ lens, type, errors }) => { +const URLInput: FC = ({ + lens, + type, + errors, + pendingURLError, + onPendingURLChange, +}) => { const interop = lens.interop(); const { fields: urls, @@ -70,6 +78,7 @@ const URLInput: FC = ({ lens, type, errors }) => { if (inputRef.current) inputRef.current.value = ""; setSelectedSite(undefined); setNewURL(""); + onPendingURLChange?.(false); }; const handleInput = (url: string) => { @@ -159,14 +168,19 @@ const URLInput: FC = ({ lens, type, errors }) => { ref={inputRef} onBlur={(e) => handleInput(e.currentTarget.value)} placeholder="URL" - onChange={(e) => setNewURL(e.currentTarget.value)} + onChange={(e) => { + const value = e.currentTarget.value; + setNewURL(value); + onPendingURLChange?.(value.trim().length > 0); + }} onPaste={handlePaste} - className="w-50" + className={`w-50 ${pendingURLError ? "is-invalid" : ""}`} /> + {pendingURLError &&
{pendingURLError}
} ); }; diff --git a/frontend/src/pages/scenes/sceneForm/SceneForm.tsx b/frontend/src/pages/scenes/sceneForm/SceneForm.tsx index 110d841ee..153521be3 100644 --- a/frontend/src/pages/scenes/sceneForm/SceneForm.tsx +++ b/frontend/src/pages/scenes/sceneForm/SceneForm.tsx @@ -66,6 +66,7 @@ const SceneForm: FC = ({ control, handleSubmit, watch, + setValue, formState: { errors }, } = useForm({ resolver: yupResolver(SceneSchema), @@ -83,6 +84,7 @@ const SceneForm: FC = ({ images: initial?.images ?? scene?.images ?? [], studio: initial?.studio ?? scene?.studio ?? undefined, tags: initial?.tags ?? scene?.tags ?? [], + pendingUrl: "", performers: (initial?.performers ?? scene?.performers ?? []).map((p) => ({ performerId: p.performer.id, name: p.performer.name, @@ -318,6 +320,7 @@ const SceneForm: FC = ({ error: errors.urls?.find?.((u) => u?.url?.message)?.url?.message, tab: "links", }, + { error: errors.pendingUrl?.message, tab: "links" }, ].filter((e) => e.error) as { error: string; tab: string }[]; return ( @@ -500,6 +503,12 @@ const SceneForm: FC = ({ lens={lens.focus("urls").defined()} type={ValidSiteTypeEnum.SCENE} errors={errors.urls} + pendingURLError={errors.pendingUrl?.message} + onPendingURLChange={(hasPendingURL) => + setValue("pendingUrl", hasPendingURL ? "pending" : "", { + shouldValidate: true, + }) + } /> setActiveTab("images")} /> diff --git a/frontend/src/pages/scenes/sceneForm/__tests__/SceneForm.test.tsx b/frontend/src/pages/scenes/sceneForm/__tests__/SceneForm.test.tsx index 2ffdf4dc2..c20272ccf 100644 --- a/frontend/src/pages/scenes/sceneForm/__tests__/SceneForm.test.tsx +++ b/frontend/src/pages/scenes/sceneForm/__tests__/SceneForm.test.tsx @@ -482,6 +482,24 @@ describe("SceneForm", () => { expect(callback).not.toHaveBeenCalled(); }); + it("blocks submit when a URL is entered but not added", async () => { + const callback = vi.fn(); + const { user } = renderEdit(callback); + await user.click(screen.getByRole("tab", { name: "Links" })); + const urlInput = (await waitFor(() => { + const el = document.querySelector('.URLInput input[placeholder="URL"]'); + if (!el) throw new Error("URLInput not ready"); + return el; + })) as HTMLInputElement; + await user.type(urlInput, "https://pending.example"); + await submit(user); + const matches = await screen.findAllByText( + "Click Add to include the entered URL before submitting", + ); + expect(matches.length).toBeGreaterThan(0); + expect(callback).not.toHaveBeenCalled(); + }); + it("disables submit when saving=true", async () => { const { user } = renderForm( , diff --git a/frontend/src/pages/scenes/sceneForm/schema.ts b/frontend/src/pages/scenes/sceneForm/schema.ts index c849e9b45..4a3986ebe 100644 --- a/frontend/src/pages/scenes/sceneForm/schema.ts +++ b/frontend/src/pages/scenes/sceneForm/schema.ts @@ -121,6 +121,13 @@ export const SceneSchema = yup.object({ }), ) .ensure(), + pendingUrl: yup + .string() + .test( + "no-pending-url", + "Click Add to include the entered URL before submitting", + (value) => !value, + ), note: yup.string().required("Edit note is required"), }); From a552f9ad8f28cb8d054f6f2c7562018a35305cfd Mon Sep 17 00:00:00 2001 From: smith113-p <205463041+smith113-p@users.noreply.github.com> Date: Fri, 29 May 2026 14:01:42 -0400 Subject: [PATCH 2/3] Block performer submit with pending URL --- .../performers/performerForm/PerformerForm.tsx | 8 ++++++++ .../__tests__/PerformerForm.test.tsx | 18 ++++++++++++++++++ .../pages/performers/performerForm/schema.ts | 7 +++++++ 3 files changed, 33 insertions(+) diff --git a/frontend/src/pages/performers/performerForm/PerformerForm.tsx b/frontend/src/pages/performers/performerForm/PerformerForm.tsx index 89c65491d..7c6e1a0ea 100644 --- a/frontend/src/pages/performers/performerForm/PerformerForm.tsx +++ b/frontend/src/pages/performers/performerForm/PerformerForm.tsx @@ -183,6 +183,7 @@ const PerformerForm: FC = ({ piercings: initial?.piercings ?? performer?.piercings ?? [], images: initial?.images ?? performer?.images ?? [], urls: initial?.urls ?? performer?.urls ?? [], + pendingUrl: "", }, }); @@ -300,6 +301,7 @@ const PerformerForm: FC = ({ error: errors.urls?.find?.((u) => u?.url?.message)?.url?.message, tab: "links", }, + { error: errors.pendingUrl?.message, tab: "links" }, ].filter((e) => e.error) as { error: string; tab: string }[]; return ( @@ -668,6 +670,12 @@ const PerformerForm: FC = ({ lens={lens.focus("urls").defined()} type={ValidSiteTypeEnum.PERFORMER} errors={errors.urls} + pendingURLError={errors.pendingUrl?.message} + onPendingURLChange={(hasPendingURL) => + setValue("pendingUrl", hasPendingURL ? "pending" : "", { + shouldValidate: true, + }) + } /> setActiveTab("images")} /> diff --git a/frontend/src/pages/performers/performerForm/__tests__/PerformerForm.test.tsx b/frontend/src/pages/performers/performerForm/__tests__/PerformerForm.test.tsx index 9ba796787..30414ae3a 100644 --- a/frontend/src/pages/performers/performerForm/__tests__/PerformerForm.test.tsx +++ b/frontend/src/pages/performers/performerForm/__tests__/PerformerForm.test.tsx @@ -714,6 +714,24 @@ describe("PerformerForm", () => { expect(callback).not.toHaveBeenCalled(); }); + it("blocks submit when a URL is entered but not added", async () => { + const callback = vi.fn(); + const { user } = renderEdit(callback); + await user.click(screen.getByRole("tab", { name: "Links" })); + const urlInput = (await waitFor(() => { + const el = document.querySelector('.URLInput input[placeholder="URL"]'); + if (!el) throw new Error("URLInput not ready"); + return el; + })) as HTMLInputElement; + await user.type(urlInput, "https://example.org/pending"); + await submit(user); + const matches = await screen.findAllByText( + "Click Add to include the entered URL before submitting", + ); + expect(matches.length).toBeGreaterThan(0); + expect(callback).not.toHaveBeenCalled(); + }); + it("hides breast type when gender is MALE", async () => { const { user } = renderEdit(); expect(screen.getByLabelText("Breast type")).toBeInTheDocument(); diff --git a/frontend/src/pages/performers/performerForm/schema.ts b/frontend/src/pages/performers/performerForm/schema.ts index 532fa0c4b..6e09f08cb 100644 --- a/frontend/src/pages/performers/performerForm/schema.ts +++ b/frontend/src/pages/performers/performerForm/schema.ts @@ -157,6 +157,13 @@ export const PerformerSchema = yup.object({ }), ) .ensure(), + pendingUrl: yup + .string() + .test( + "no-pending-url", + "Click Add to include the entered URL before submitting", + (value) => !value, + ), note: yup.string().required("Edit note is required"), }); From fe875e9e2c5ed3b133a4eab20ce7b0e9ca794107 Mon Sep 17 00:00:00 2001 From: smith113-p <205463041+smith113-p@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:18:35 +0200 Subject: [PATCH 3/3] Address review comments Also does some renaming for clarity. --- frontend/src/components/urlInput/urlInput.tsx | 18 ++++++++--------- frontend/src/hooks/index.ts | 2 ++ frontend/src/hooks/usePendingURLField.ts | 20 +++++++++++++++++++ .../performerForm/PerformerForm.tsx | 9 +++------ .../src/pages/scenes/sceneForm/SceneForm.tsx | 9 +++------ 5 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 frontend/src/hooks/usePendingURLField.ts diff --git a/frontend/src/components/urlInput/urlInput.tsx b/frontend/src/components/urlInput/urlInput.tsx index 195eee251..605a3a911 100644 --- a/frontend/src/components/urlInput/urlInput.tsx +++ b/frontend/src/components/urlInput/urlInput.tsx @@ -32,7 +32,7 @@ interface URLInputProps { type: ValidSiteTypeEnum; errors?: ErrorsType; pendingURLError?: string; - onPendingURLChange?: (hasPendingURL: boolean) => void; + onPendingURLChange?: (pendingURL: string) => void; } const URLInput: FC = ({ @@ -63,6 +63,11 @@ const URLInput: FC = ({ s.valid_types.includes(type), ); + const setPendingURL = (url: string) => { + setNewURL(url); + onPendingURLChange?.(url.trim()); + }; + const handleAdd = () => { if (!newURL || !selectedSite) return; const cleanedURL = cleanURL(selectedSite?.regex, newURL); @@ -77,8 +82,7 @@ const URLInput: FC = ({ if (selectRef.current) selectRef.current.value = ""; if (inputRef.current) inputRef.current.value = ""; setSelectedSite(undefined); - setNewURL(""); - onPendingURLChange?.(false); + setPendingURL(""); }; const handleInput = (url: string) => { @@ -98,6 +102,7 @@ const URLInput: FC = ({ const updatedURL = cleanURL(site.regex, url); if (updatedURL) { inputRef.current.value = updatedURL; + setPendingURL(updatedURL); return true; } } @@ -108,7 +113,6 @@ const URLInput: FC = ({ const match = handleInput(e.clipboardData.getData("text/plain")); if (match) { e.preventDefault(); - setNewURL(e.currentTarget.value); } }; @@ -168,11 +172,7 @@ const URLInput: FC = ({ ref={inputRef} onBlur={(e) => handleInput(e.currentTarget.value)} placeholder="URL" - onChange={(e) => { - const value = e.currentTarget.value; - setNewURL(value); - onPendingURLChange?.(value.trim().length > 0); - }} + onChange={(e) => setPendingURL(e.currentTarget.value)} onPaste={handlePaste} className={`w-50 ${pendingURLError ? "is-invalid" : ""}`} /> diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 8cffd8c19..b9a9403a1 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,7 +1,9 @@ export { default as useAuth } from "./useAuth"; +export { useBeforeUnload } from "./useBeforeUnload"; export { useCurrentUser } from "./useCurrentUser"; export { default as useEditFilter } from "./useEditFilter"; export { default as usePagination } from "./usePagination"; +export { usePendingURLField } from "./usePendingURLField"; export { useQueryParams } from "./useQueryParams"; export { useToast } from "./useToast"; export { useUser } from "./useUser"; diff --git a/frontend/src/hooks/usePendingURLField.ts b/frontend/src/hooks/usePendingURLField.ts new file mode 100644 index 000000000..110992602 --- /dev/null +++ b/frontend/src/hooks/usePendingURLField.ts @@ -0,0 +1,20 @@ +import { useCallback } from "react"; +import type { + FieldValues, + Path, + PathValue, + UseFormSetValue, +} from "react-hook-form"; + +export const usePendingURLField = ( + setValue: UseFormSetValue, + name: Path, +) => + useCallback( + (pendingURL: string) => { + setValue(name, pendingURL as PathValue>, { + shouldValidate: true, + }); + }, + [name, setValue], + ); diff --git a/frontend/src/pages/performers/performerForm/PerformerForm.tsx b/frontend/src/pages/performers/performerForm/PerformerForm.tsx index 7c6e1a0ea..4a1107c28 100644 --- a/frontend/src/pages/performers/performerForm/PerformerForm.tsx +++ b/frontend/src/pages/performers/performerForm/PerformerForm.tsx @@ -34,7 +34,7 @@ import { type PerformerEditOptionsInput, ValidSiteTypeEnum, } from "src/graphql"; -import { useBeforeUnload } from "src/hooks/useBeforeUnload"; +import { useBeforeUnload, usePendingURLField } from "src/hooks"; import DiffPerformer from "./diff"; import ExistingPerformerAlert from "./ExistingPerformerAlert"; import { type PerformerFormData, PerformerSchema } from "./schema"; @@ -188,6 +188,7 @@ const PerformerForm: FC = ({ }); const lens = useLens({ control }); + const onPendingURLChange = usePendingURLField(setValue, "pendingUrl"); const [activeTab, setActiveTab] = useState("personal"); const [updateAliases, setUpdateAliases] = useState( @@ -671,11 +672,7 @@ const PerformerForm: FC = ({ type={ValidSiteTypeEnum.PERFORMER} errors={errors.urls} pendingURLError={errors.pendingUrl?.message} - onPendingURLChange={(hasPendingURL) => - setValue("pendingUrl", hasPendingURL ? "pending" : "", { - shouldValidate: true, - }) - } + onPendingURLChange={onPendingURLChange} /> setActiveTab("images")} /> diff --git a/frontend/src/pages/scenes/sceneForm/SceneForm.tsx b/frontend/src/pages/scenes/sceneForm/SceneForm.tsx index 153521be3..8878ec994 100644 --- a/frontend/src/pages/scenes/sceneForm/SceneForm.tsx +++ b/frontend/src/pages/scenes/sceneForm/SceneForm.tsx @@ -29,7 +29,7 @@ import { type SceneEditDetailsInput, ValidSiteTypeEnum, } from "src/graphql"; -import { useBeforeUnload } from "src/hooks/useBeforeUnload"; +import { useBeforeUnload, usePendingURLField } from "src/hooks"; import { formatDuration, parseDuration, performerHref } from "src/utils"; import DiffScene from "./diff"; import ExistingSceneAlert from "./ExistingSceneAlert"; @@ -108,6 +108,7 @@ const SceneForm: FC = ({ }); const lens = useLens({ control }); + const onPendingURLChange = usePendingURLField(setValue, "pendingUrl"); const fieldData = watch(); const [oldSceneChanges, newSceneChanges] = useMemo( @@ -504,11 +505,7 @@ const SceneForm: FC = ({ type={ValidSiteTypeEnum.SCENE} errors={errors.urls} pendingURLError={errors.pendingUrl?.message} - onPendingURLChange={(hasPendingURL) => - setValue("pendingUrl", hasPendingURL ? "pending" : "", { - shouldValidate: true, - }) - } + onPendingURLChange={onPendingURLChange} /> setActiveTab("images")} />