| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useCallback, useEffect, useState } from "react"; |
| import { UNAUTHORIZED_EVENT, signal } from "../apiContract"; |
| import type { DrillDescriptor, PageEnvelope } from "../ui/types"; |
| import { DrillPanel } from "./DrillPanel"; |
| import { PageView } from "./PageView"; |
| import { fetchPage } from "./pageApi"; |
|
|
| type State = |
| | { phase: "loading" } |
| | { phase: "ready"; page: PageEnvelope } |
| | { phase: "error"; message: string }; |
|
|
| export function PageSurface({ pageKey, label }: { pageKey: string; label: string }) { |
| const [state, setState] = useState<State>({ phase: "loading" }); |
| const [params, setParams] = useState<Record<string, string>>({}); |
| const [busy, setBusy] = useState(false); |
| const [drill, setDrill] = useState<DrillDescriptor | null>(null); |
|
|
| |
| |
| |
| const paramKey = JSON.stringify(params); |
|
|
| useEffect(() => { |
| let dead = false; |
| setBusy(true); |
| void fetchPage(pageKey, JSON.parse(paramKey) as Record<string, string>).then((r) => { |
| if (dead) return; |
| setBusy(false); |
| if (r.ok) { |
| setState({ phase: "ready", page: r.page }); |
| return; |
| } |
| |
| |
| |
| if (r.status === 401) { |
| signal(UNAUTHORIZED_EVENT); |
| return; |
| } |
| setState({ phase: "error", message: r.message }); |
| }); |
| return () => { |
| dead = true; |
| }; |
| }, [pageKey, paramKey]); |
|
|
| |
| |
| |
| const onControl = useCallback((key: string, value: string) => { |
| setDrill(null); |
| setParams((p) => ({ ...p, [key]: value })); |
| }, []); |
|
|
| if (state.phase === "loading") { |
| return ( |
| <div className="pg-state"> |
| {/* β THE TITLE STAYS, THE SENTENCE GOES (wave 17 item 3/R6). R6 deletes |
| loading COPY β "Loading live figures from Odoo. The first read of a |
| new scope takes a moment." was an apology for a wait the mark states |
| without excusing. The heading is not loading copy: it is this page's |
| name, already chosen in the rail, and dropping it would make arrival |
| a title POP-IN on a surface that had been anonymous. */} |
| <h1>{label}</h1> |
| <span className="lp-spin lp-spin--lg" role="status" aria-label="Loading" /> |
| </div> |
| ); |
| } |
|
|
| if (state.phase === "error") { |
| return ( |
| <div className="pg-state pg-error"> |
| <h1>{label}</h1> |
| <p>{state.message}</p> |
| <button |
| className="login-submit shell-retry" |
| type="button" |
| onClick={() => setParams((p) => ({ ...p }))} |
| > |
| Try again |
| </button> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className={busy ? "pg-host is-busy" : "pg-host"}> |
| <PageView page={state.page} onControl={onControl} onOpen={setDrill} busy={busy} /> |
| {drill ? <DrillPanel drill={drill} onClose={() => setDrill(null)} /> : null} |
| </div> |
| ); |
| } |
|
|