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:
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user