Root location folds away once there is nothing left to do

Everything under it is in order most of the time, and a card that is four
paragraphs of "fine" pushes the settings that are not fine off the screen. So it
shuts, with a check on the right, and opens on a click.

"Finished" is three bits reported upward by the parts that own them: the path is
saved, the checkout is a repository, and media/ both exists and is current. None
of those is knowable in the parent — each child fetches its own status — and
lifting all of it up to answer one question would be a far worse trade than
three booleans.

It is only collapsible while settled. A block you can shut on unfinished setup
is a way to hide the thing you came here to fix. And the children are NOT
RENDERED while it is shut rather than hidden, because a collapsed block whose
children still poll is a settled panel making requests all afternoon about
something nobody is looking at.

The `null` in `collapsed` is a sentinel for "nobody has decided", which is what
lets a deliberate expand survive. Setting it to false while unsettled spent it
on the first render — before any status had arrived — so the block could never
fold itself away at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 11:52:54 +01:00
co-authored by Claude Opus 5
parent f30d40446a
commit cc283ee7b6
5 changed files with 139 additions and 14 deletions
+48 -4
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Check, FolderRoot, Loader2, Pencil } from 'lucide-react';
import { Check, CircleCheck, FolderRoot, Loader2, Pencil } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -63,6 +63,36 @@ export const RootLocationBlock = () => {
const [forbidden, setForbidden] = useState(false);
const [flashing, flash] = useFlash();
/*
Finished, and therefore worth folding away.
Three bits reported up by the parts that own them: the path is saved, the checkout is a repository,
and `media/` both exists and is current. None of those is knowable here — each child fetches its
own status, and lifting all of it into this component to answer one question would be a much worse
trade than three booleans.
*/
const [websiteReady, setWebsiteReady] = useState(false);
const [mediaReady, setMediaReady] = useState(false);
const settled = Boolean(saved?.isSet) && websiteReady && mediaReady && !editing;
/*
Shut on the first arrival at a settled block, and open the moment anything needs attention.
`null` means nobody has decided yet, which is what lets a deliberate expand SURVIVE — once somebody
has opened it, `collapsed` is false and this effect will not run again while it stays settled. A
plain `useState(true)` would slam it shut on every re-render that touched the dependency.
*/
const [collapsed, setCollapsed] = useState<boolean | null>(null);
useEffect(() => {
setCollapsed((chosen) => {
// Not settled yet: force it open if somebody had shut it, but LEAVE `null` alone. Writing false
// here was a bug — `settled` is false on the first render, so the sentinel was spent before it
// could mean anything and the block never folded itself away.
if (!settled) return chosen === null ? null : false;
return chosen === null ? true : chosen;
});
}, [settled]);
const load = useCallback(async () => {
try {
const current = await clientRef.current.get<Settings>('/fofos-admin/_config/root');
@@ -99,7 +129,17 @@ export const RootLocationBlock = () => {
};
const shell = (children: React.ReactNode, status?: React.ReactNode) => (
<Block icon={FolderRoot} title="Root location" hint="Where the fofos site is on this machine." status={status}>
<Block
icon={FolderRoot}
title="Root location"
hint="Where the fofos site is on this machine."
status={status}
// Only offered once there is nothing to do. A block you can shut on unfinished setup is a way to
// hide the thing you came here to fix.
collapsible={settled}
collapsed={settled && collapsed !== false}
onCollapsedChange={setCollapsed}
>
{children}
</Block>
);
@@ -164,11 +204,11 @@ export const RootLocationBlock = () => {
path are one question — split apart, somebody could change the root here and read a status
about the old one there, with both looking equally current. Hidden while editing: it
describes what is SAVED, and a half-typed path has nothing at it. */}
{!editing && <WebsiteCheckout rootPath={saved.rootPath} />}
{!editing && <WebsiteCheckout rootPath={saved.rootPath} onSettled={setWebsiteReady} />}
{/* Beside the checkout, under the same root — a backup of the root is then the whole site, the
code and the things it serves. Hidden while editing for the same reason as the checkout. */}
{!editing && <MediaFolder rootPath={saved.rootPath} />}
{!editing && <MediaFolder rootPath={saved.rootPath} onSettled={setMediaReady} />}
{/* The block's bottom row. Everything you can DO is here, on its own line, so the path above it
gets the full width. */}
@@ -199,6 +239,10 @@ export const RootLocationBlock = () => {
<Check className="size-3.5" />
Saved
</span>
) : settled ? (
// Big, and the only thing in the row. Everything under here is in order, and a shut block whose
// whole message is "nothing to do" should say it in one glance rather than in a sentence.
<CircleCheck className="size-6 text-success" />
) : null,
);
};