commit 8b68b4759af6fd9bb0adfdc20ee14385f213aaa1
parent f45aa9be0cd817525f831a193a5029ea5a8a36ca
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 03:05:28 -0400
editor: ChannelForm create's On create boxes seed only from a refused submit of their own kind (review L1)
Fetch posts now (social) and Fetch playlist now / Add to top (video) are drawn
in alternate branches. After a refused submit from one branch, switching the URL
to the other kind mounted the other branch's box seeded from `values`, where its
name was absent, so it came up unticked; on main it came up at its default
(ticked). Each branch now seeds only from a refusal of its own kind, told by the
hidden `sourceKind=social` the social branch always posts. A new spec case
refuses a video submit and switches to a social URL (Fetch posts now ticked),
and the reverse.
Controlled.tsx's comment says what a controlled select resets to (review I2): its
options never get defaultSelected, so the reset selects the server-rendered
option, or the first when it mounted on the client.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 73 insertions(+), 10 deletions(-)
diff --git a/editor/app/channels/components/ChannelForm.tsx b/editor/app/channels/components/ChannelForm.tsx
@@ -43,6 +43,7 @@ import {
ControlledSelect,
} from "../../components/forms/Controlled";
import {
+ failedValues,
seedChecked,
seedValue,
type SeedSource,
@@ -138,6 +139,16 @@ export function ChannelForm({
"handling",
c?.handling === "transcribe" ? "transcribe" : "youtube",
);
+ // Create mode's "On create" boxes are drawn per KIND of source (Fetch posts
+ // now for a social account; Fetch playlist now and Add to top for a video
+ // channel), so each kind seeds only from a refused submit of its own kind.
+ // A refused video submit carries no `fetchPostsNow`, and switching the URL
+ // to a social account afterwards must show that box at its default
+ // (ticked), not "absent, so unticked". The social branch always posts the
+ // hidden `sourceKind=social`.
+ const postedSocial = failedValues(state)?.sourceKind === "social";
+ const socialSeed = postedSocial ? state : undefined;
+ const videoSeed = postedSocial ? undefined : state;
// Controlled, so a surface that has to compare what is TYPED against what a
// stored scan was run under can read them. (Nothing does yet; the metadata
@@ -493,7 +504,11 @@ export function ChannelForm({
<input
type="checkbox"
name="fetchPostsNow"
- defaultChecked={seedChecked(state, "fetchPostsNow", true)}
+ defaultChecked={seedChecked(
+ socialSeed,
+ "fetchPostsNow",
+ true,
+ )}
className="mt-1"
/>
<span className="flex flex-col gap-0.5">
@@ -510,7 +525,7 @@ export function ChannelForm({
<input
type="checkbox"
name="fetchPlaylist"
- defaultChecked={seedChecked(state, "fetchPlaylist", true)}
+ defaultChecked={seedChecked(videoSeed, "fetchPlaylist", true)}
className="mt-1"
/>
<span className="flex flex-col gap-0.5">
@@ -527,7 +542,7 @@ export function ChannelForm({
type="checkbox"
name="prioritizeDownload"
defaultChecked={seedChecked(
- state,
+ videoSeed,
"prioritizeDownload",
false,
)}
diff --git a/editor/app/components/forms/Controlled.tsx b/editor/app/components/forms/Controlled.tsx
@@ -18,13 +18,15 @@ import {
// back. It does not do the same for the other two kinds:
// - a checkbox or radio's `defaultChecked` is set only when it MOUNTS
// (react-dom `initInput`; `updateInput` sets `checked` alone);
-// - a select's options' `defaultSelected` likewise (`updateOptions` is
-// called with `setDefaultSelected` false on every update).
-// So after ANY submit — failure or success — the reset returns the element to
-// what it mounted with while React's state still says otherwise, until the
-// next re-render that reaches it re-asserts the prop (the editor's
-// auto-refresh, 5 s by default; never, with it off). A NAMED one submitted in
-// that window posts the stale choice.
+// - a controlled select's options never get `defaultSelected`
+// (`updateOptions` is called with `setDefaultSelected` false at mount and
+// on every update), so the reset selects the option the SERVER rendered
+// `selected`, or the first option when the select mounted on the client.
+// So after ANY submit — failure or success — the reset returns a checkbox or
+// radio to what it mounted with, and a select to that option, while React's
+// state still says otherwise, until the next re-render that reaches it
+// re-asserts the prop (the editor's auto-refresh, 5 s by default; never, with
+// it off). A NAMED one submitted in that window posts the stale choice.
//
// These put the controlled value back right after the form resets: a `reset`
// listener on the element's form, deferred a microtask so it runs after the
diff --git a/editor/e2e/forms-keep-input.spec.ts b/editor/e2e/forms-keep-input.spec.ts
@@ -314,6 +314,52 @@ test("ChannelForm create: a refused slug keeps every field", async ({
await expect(named(form, "sleepBetweenDownloadsSeconds")).toHaveValue("9");
});
+// Create mode draws its "On create" boxes per KIND of source: Fetch posts now
+// for a social account, Fetch playlist now for a video channel. A box of the
+// kind that was NOT posted must come back at its default (ticked) when the URL
+// switches kind after a refusal — not read as "absent from values, unticked".
+test("ChannelForm create: switching the source's kind after a refusal shows the other kind's box at its default", async ({
+ page,
+}) => {
+ const BSKY_URL = "https://bsky.app/profile/someone.bsky.social";
+ const VIDEO_URL = "https://www.youtube.com/@kindswitch/videos";
+ await resetData("one-youtube-channel");
+ await generateReport(page, "new");
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: /create channel/i }),
+ });
+ const posts = named(form, "fetchPostsNow");
+ const playlist = named(form, "fetchPlaylist");
+ const refuse = async () => {
+ await form.getByRole("button", { name: /create channel/i }).click();
+ await expect(form.getByText(/already exists/i)).toBeVisible();
+ };
+
+ // A refused VIDEO submit, then a social URL.
+ await page.goto("/channels/new");
+ await hydrated(form);
+ await named(form, "name").fill("Kind Switch");
+ await named(form, "slug").fill("test-youtube"); // already exists
+ await named(form, "url").fill(VIDEO_URL);
+ await playlist.uncheck();
+ await refuse();
+ await expect(playlist).not.toBeChecked();
+ await named(form, "url").fill(BSKY_URL);
+ await expect(posts).toBeChecked();
+
+ // The reverse: a refused SOCIAL submit, then a video URL.
+ await page.goto("/channels/new");
+ await hydrated(form);
+ await named(form, "name").fill("Kind Switch");
+ await named(form, "slug").fill("test-youtube");
+ await named(form, "url").fill(BSKY_URL);
+ await posts.uncheck();
+ await refuse();
+ await expect(posts).not.toBeChecked();
+ await named(form, "url").fill(VIDEO_URL);
+ await expect(playlist).toBeChecked();
+});
+
test("ChannelForm edit: a refused filter keeps every field", async ({
page,
}) => {