From ba67083bec308fc1c2e5a985661cfd67ab4d6667 Mon Sep 17 00:00:00 2001 From: dawn Date: Tue, 22 Sep 2026 17:17:42 +0300 Subject: [PATCH] web: draft-over-server form state for the sites settings panel Signed-off-by: dawn --- .../[repo]/settings/sites/+page.svelte | 111 ++++++------------ 1 file changed, 38 insertions(+), 73 deletions(-) diff --git a/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte b/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte index ff5c81a7f..42c64b82e 100644 --- a/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte +++ b/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte @@ -34,6 +34,7 @@ let { data } = $props(); const auth = getAuth(); const sitesUrl = $derived(page.data.publicConfig?.sitesUrl as string | undefined); + const owner = $derived(data.access.canAdminister); const errMessage = (err: unknown, fallback: string): string => { if (err instanceof ClientResponseError) return err.description || err.error || fallback; @@ -67,77 +68,28 @@ (cause) => errMessage(cause, "Failed to load site configuration.") ); - // the auto-run load can settle before the oauth agent hydrates (null result); - // re-run ONCE when the agent becomes available so the claim/config resolve. - let attemptedHydrationRetry = false; - $effect(() => { - if ( - auth.agent && - !attemptedHydrationRetry && - loaded.data === null && - !loaded.loading - ) { - attemptedHydrationRetry = true; - void loaded.run(); - } - }); - - // same-route navigation to a different repository resets the form and - // reloads the claim/config for it - let repoKey = $state(""); - $effect(() => { - void Promise.resolve(data.repo).then((r) => { - const key = r.repoDid ?? ""; - if (key && key !== repoKey) { - repoKey = key; - resetForm(); - void loaded.run(); - } - }); - }); - - // the canonical seed: what the form should show once the config is known + // createLoad reruns on auth.agent, sitesUrl and data.repo, so the panel needs no + // retry of its own: while the agent is null the load returns null and the skeleton holds type SiteForm = { branch: string; directory: string; type: "index" | "subpath" }; - const EMPTY_FORM: SiteForm = { branch: "", directory: "/", type: "subpath" }; - const seed = $derived.by((): SiteForm => { + // the settled config is the one seed; an unconfigured repo prefills the knot default + const current = $derived.by((): SiteForm => { const config = loaded.data?.config; return { - branch: config?.branch ?? "", + branch: config?.branch ?? loaded.data?.defaultBranch ?? "", directory: config?.dir ?? "/", type: (config?.isIndex ? "index" : "subpath") as "index" | "subpath" }; }); - let form = $state({ ...EMPTY_FORM }); - const dirty = $derived(JSON.stringify(form) !== JSON.stringify(seed)); - const resetForm = () => { - form.branch = EMPTY_FORM.branch; - form.directory = EMPTY_FORM.directory; - form.type = EMPTY_FORM.type; - }; - const reset = () => { - form.branch = seed.branch; - form.directory = seed.directory; - form.type = seed.type; - }; - - // seed the untouched form from the settled config - $effect(() => { - if (loaded.data !== null && loaded.data !== undefined) { - if (JSON.stringify(form) === JSON.stringify(EMPTY_FORM)) { - reset(); - } - } - }); - // unconfigured repos prefill the knot's default branch - $effect(() => { - if ( - loaded.data?.config === null && - loaded.data.defaultBranch && - form.branch === "" - ) { - form.branch = loaded.data.defaultBranch; - } - }); + let picked = $state(); + const form = $derived(picked ?? current); + const sameForm = (a: SiteForm, b: SiteForm) => + a.branch === b.branch && a.directory === b.directory && a.type === b.type; + // an unconfigured repo has a pending change: saving the prefilled branch is what + // creates the site, so the save bar is live before the first edit + const unconfigured = $derived(loaded.data?.config === null); + const dirty = $derived(picked !== undefined ? !sameForm(picked, current) : unconfigured); + const pick = (patch: Partial) => (picked = { ...form, ...patch }); + const reset = () => (picked = undefined); const branches = $derived.by(() => { const options: { value: string; label?: string }[] = []; @@ -203,8 +155,8 @@ {#snippet action()} - {#if auth.agent?.sub === repo.ownerDid && domain && loaded.data !== null} - + {#if owner && domain && loaded.data !== null} + {/if} {/snippet} @@ -220,11 +172,11 @@ {@const actionError = save.error ?? disable.error} - {#if loaded.loading || !auth.agent || loaded.data === null} + {#if loaded.loading || loaded.data === null} -{:else if auth.agent.sub !== repo.ownerDid} +{:else if !owner} {:else if loaded.error} @@ -264,7 +216,7 @@ > + form.directory, (value) => pick({ directory: value })} + placeholder="/" + disabled={save.loading} + class="w-full sm:w-80" + />
- Index site + pick({ type: "index" })} + name="site-type">Index site {domain}
- Sub-path site pick({ type: "subpath" })} + name="site-type">Sub-path site {domain}/{repo.name}
-- 2.51.2