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