feat(documents): browse all data definitions per published process

New Documents scene at landing chip + scene route. Walks the
curated startable flows, fetches each one's graph, surfaces every
data_definition as a row with its source process. Search filter,
two-column list+detail layout. Mobile collapses to single column
at 700px.
This commit is contained in:
2026-06-14 16:23:31 +04:00
parent 5072e65ec3
commit c9deaeb5c2
5 changed files with 176 additions and 1 deletions
+139
View File
@@ -0,0 +1,139 @@
import { useEffect, useMemo, useState } from "react";
import { useApp } from "../state/store";
import { api } from "../lib/api";
import { curatedPublishedFlows, fetchStartableFlows } from "../lib/flowCuration";
import { Branch, Layers } from "../components/icons";
interface Document {
_key: string;
name: string;
label: string;
description: string;
flow_key: string;
flow_label: string;
}
async function loadDocuments(): Promise<Document[]> {
const token = sessionStorage.getItem("fm.mc.token.v1") || "";
const catalogue = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json()).catch(() => ({ items: [] }));
const fromList = curatedPublishedFlows((catalogue?.items || []) as any[]);
const directHits = await fetchStartableFlows(api.config.baseUrl, token);
const merged = new Map<string, any>();
for (const it of [...directHits, ...fromList]) merged.set(it._key, it);
const flows = Array.from(merged.values()).slice(0, 10);
const all: Document[] = [];
for (const f of flows) {
try {
const g = await api.graph(f._key);
if (!g) continue;
const dataDefs = (g as any).data_definitions || [];
for (const d of dataDefs) {
all.push({
_key: d._key,
name: d.name || "",
label: d.label || d.display_name || d.name || "Document",
description: d.description || "",
flow_key: f._key,
flow_label: f.display_name || f.name,
});
}
} catch { /* ignore */ }
}
return all;
}
export default function Documents() {
const setScene = useApp((s) => s.setScene);
const pushToast = useApp((s) => s.pushToast);
const [docs, setDocs] = useState<Document[] | null>(null);
const [filter, setFilter] = useState("");
const [activeKey, setActiveKey] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
loadDocuments()
.then((rows) => !cancelled && setDocs(rows))
.catch((err) => !cancelled && pushToast("err", `Documents failed: ${err.message}`));
return () => { cancelled = true; };
}, []);
const visible = useMemo(() => {
if (!docs) return [];
const q = filter.toLowerCase().trim();
if (!q) return docs;
return docs.filter((d) =>
d.label.toLowerCase().includes(q) ||
d.description.toLowerCase().includes(q) ||
d.flow_label.toLowerCase().includes(q)
);
}, [docs, filter]);
const active = visible.find((d) => d._key === activeKey) || null;
return (
<div className="docs-scene">
<header className="docs-head">
<div className="mc-hero-eyebrow"><Layers size={12} /> Documents</div>
<h2 className="mc-hero-title">Everything your processes capture</h2>
<p className="docs-intro">
One document per data shape per published process. Each row points back to the process it belongs to. This is what your forms and rules read and write.
</p>
<input
className="docs-search"
type="search"
placeholder="Search documents and processes…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</header>
<div className="docs-split">
<section className="docs-list">
<div className="hub-section-label">DOCUMENTS · {visible.length}</div>
{docs === null && <div className="hub-empty">Loading from EA2</div>}
{docs !== null && visible.length === 0 && (
<div className="hub-empty">
No documents match. Open the <button className="link-inline" onClick={() => setScene("studio")}>Process Studio</button> to publish a new flow.
</div>
)}
<ul className="docs-rows">
{visible.slice(0, 100).map((d) => (
<li key={d._key}>
<button
className={`docs-row ${d._key === activeKey ? "active" : ""}`}
onClick={() => setActiveKey(d._key)}
>
<div className="docs-row-title"><Branch size={11} /> {d.label}</div>
<div className="docs-row-meta">{d.flow_label}</div>
</button>
</li>
))}
</ul>
</section>
<section className="docs-detail">
<div className="hub-section-label">DETAIL</div>
{!active && <div className="hub-empty">Pick a document on the left to read its description and source process.</div>}
{active && (
<div className="docs-card">
<div className="docs-card-title">{active.label}</div>
<div className="docs-card-meta">From process · {active.flow_label}</div>
{active.description && <p className="docs-card-body">{active.description}</p>}
<div className="docs-card-actions">
<button
className="btn btn-secondary"
onClick={() => { setScene("studio"); }}
>
Open in Studio
</button>
</div>
</div>
)}
</section>
</div>
</div>
);
}
+1
View File
@@ -85,6 +85,7 @@ export default function Landing() {
<button className="hub-chip" onClick={() => setScene("agent")}>Command Assistant</button>
<button className="hub-chip" onClick={() => setScene("chat")}>Team Chat</button>
<button className="hub-chip" onClick={() => setScene("approvals")}>Approvals queue</button>
<button className="hub-chip" onClick={() => setScene("documents")}>Documents</button>
<button className="hub-chip" onClick={() => setScene("explainer")}>What is FlowMaster?</button>
</div>