loopable / web /src /automation /PresetPlan.tsx
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
2183dbe verified
Raw
History Blame Contribute Delete
5.34 kB
// ---------------------------------------------------------------------------
// automation/PresetPlan.tsx β€” WAVE 25 item 4 (ruling R2b, contract C1):
// WHICH COLUMNS THIS WILL USE, AND WHICH IT WILL CREATE.
//
// The owner's words: *"since this is a global fields all tenant use its really
// just appending it and adding it to the custom database"* β€” so the Create
// record action's configuration has to SHOW the two lists before anything is
// spent, on whatever database that action names, including a hand-made one.
//
// β›” IT RENDERS THE SERVER'S ANSWER AND COMPUTES NOTHING (C1: "the client
// renders this; it does not compute it"). Diffing the preset set against a
// table's columns here would be a second implementation of `ut_ensure`'s merge
// rule β€” and it would be wrong exactly when the two lists differ, which is the
// only case this panel exists to answer.
//
// ⚠ ITS OWN FILE, AND THAT IS THE BUILDER'S LAW BEING KEPT RATHER THAN BENT.
// `AutomationBuilder` renders SYNCHRONOUSLY from what it was handed (C14 leg 1
// β€” the ghost fix: a view that can be in two shapes shows you the wrong one).
// This panel genuinely needs a fetch, so it owns one, keeps a FIXED-SHAPE
// skeleton while it is in flight, and never changes the shape of anything above
// it. Exactly the split the Board already makes.
// ---------------------------------------------------------------------------
import { useEffect, useState } from "react";
import { getPresetPlan } from "./automationApi";
import type { PresetPlan as Plan } from "./automationApi";
type Load =
| { phase: "idle" }
| { phase: "loading" }
| { phase: "ready"; plan: Plan }
| { phase: "error" };
/**
* ⚠ ONE LINE PER LIST, NOT A TOUR (DESIGN.md 4 / R13). The panel says what will happen and shows
* the column names; it does not explain what a preset is, why the set is shared across tenants,
* or what enrichment does. Each of those has a home β€” the action's own `detail`, the field's grey
* machine-owned mark, the run log β€” and repeating them here is the paragraph nobody reads.
*/
function List({ title, fields }: { title: string; fields: Plan["fields"] }) {
if (!fields.length) return null;
return (
<div className="autox-preset-list">
<p className="autox-preset-head">
{title} <span className="autox-preset-n">{fields.length}</span>
</p>
<div className="autox-preset-chips">
{fields.map((f) => (
<span className="autox-preset-chip" key={f.key} title={`${f.key} Β· ${f.type}`}>
{f.label}
</span>
))}
</div>
</div>
);
}
export default function PresetPlan({ table }: { table: string }) {
const [load, setLoad] = useState<Load>({ phase: "idle" });
useEffect(() => {
if (!table) {
setLoad({ phase: "idle" });
return;
}
const ac = new AbortController();
setLoad({ phase: "loading" });
getPresetPlan(table, ac.signal)
.then((plan) => setLoad({ phase: "ready", plan }))
.catch((e) => {
if ((e as Error)?.name === "AbortError") return;
setLoad({ phase: "error" });
});
return () => ac.abort();
}, [table]);
// β›” NO DATABASE, NO CLAIM. With nothing chosen there is no question to answer, so this renders
// nothing at all rather than an empty heading β€” a heading over nothing says "there should be
// something here", which is a different and false statement (R13, and the same reasoning R14
// applies to the whole Configuration section).
if (!table || load.phase === "idle") return null;
if (load.phase === "loading") {
return (
<p className="auto-note" role="status">
<span className="lp-spin" aria-label="Loading" />
</p>
);
}
// ⚠ HONEST, AND IT DOES NOT GUESS. A failed fetch is not "no columns will be created" β€” that
// would be a specific, checkable claim about the user's database, invented to keep a panel
// tidy ([[no-unverifiable-aggregates]]). It says the server did not answer.
if (load.phase === "error") {
return <p className="auto-note">Could not read this database's columns just now.</p>;
}
const { plan } = load;
return (
<div className="autox-preset">
{/*
⚠ THREE STATES, AND THE FIRST TWO ARE DIFFERENT FACTS. A database that does not exist yet
(R10's spawn-on-save moment β€” the action names a table nobody has made) is not the same as
one that exists and shares none of the columns, even though both render an empty "already
here" list. The server answers `exists` precisely so this line does not have to infer it
from a length.
*/}
{!plan.exists ? (
<p className="auto-hint">
This database does not exist yet. Saving makes it, with these columns.
</p>
) : null}
<List title="Already in this database" fields={plan.willUse} />
<List title="Will be created" fields={plan.willCreate} />
{/* Both lists empty on a table that DOES exist means the server offered no preset set at
all. Saying nothing here would leave the panel looking like it failed to load. */}
{plan.exists && !plan.willUse.length && !plan.willCreate.length ? (
<p className="auto-note">This server offered no preset columns.</p>
) : null}
</div>
);
}