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
+2
View File
@@ -14,6 +14,7 @@ import Hub from "./scenes/Hub";
import GeoAttendance from "./scenes/GeoAttendance";
import Explainer from "./scenes/Explainer";
import Approvals from "./scenes/Approvals";
import Documents from "./scenes/Documents";
import CommandBar from "./components/CommandBar";
import Toaster from "./components/Toaster";
import Console from "./components/Console";
@@ -169,6 +170,7 @@ export default function App() {
{scene === "geo-attendance" && <GeoAttendance />}
{scene === "explainer" && <Explainer />}
{scene === "approvals" && <Approvals />}
{scene === "documents" && <Documents />}
{scene === "settings" && <Settings />}
</div>
+33
View File
@@ -2088,3 +2088,36 @@ select.studio-input { background: var(--bp-paper); }
/* Generic scene container padding. */
.hub-scene, .approvals-scene, .geo-scene, .explainer-scene { padding: 16px 12px; }
}
.docs-scene { padding: 24px; max-width: 1400px; margin: 0 auto; background: var(--bp-paper); min-height: calc(100vh - 60px); }
.docs-head { margin-bottom: 20px; max-width: 880px; }
.docs-intro { font-size: 13px; color: var(--bp-muted); line-height: 1.5; margin-top: 6px; }
.docs-search {
margin-top: 14px;
width: 100%;
max-width: 480px;
padding: 8px 12px;
background: var(--bp-paper);
border: 1px solid var(--bp-navy);
color: var(--bp-navy);
font-family: var(--bp-mono);
font-size: 12px;
}
.docs-split { display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; }
.docs-list, .docs-detail { border: 1px solid var(--bp-navy); padding: 16px; background: var(--bp-paper); display: flex; flex-direction: column; gap: 10px; }
.docs-rows { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; max-height: 70vh; overflow-y: auto; }
.docs-row { width: 100%; text-align: left; padding: 10px 12px; background: var(--bp-paper); border: 1px solid color-mix(in srgb, var(--bp-navy) 18%, transparent); color: var(--bp-navy); cursor: pointer; display: flex; flex-direction: column; gap: 4px; }
.docs-row:hover { background: color-mix(in srgb, var(--bp-amber) 12%, var(--bp-paper)); }
.docs-row.active { background: color-mix(in srgb, var(--bp-amber) 28%, var(--bp-paper)); }
.docs-row-title { font-size: 13px; font-weight: 700; display: flex; align-items: center; gap: 6px; }
.docs-row-meta { font-family: var(--bp-mono); font-size: 10px; color: var(--bp-muted); }
.docs-card { padding: 14px; background: var(--bp-paper); border: 1px solid var(--bp-navy); display: flex; flex-direction: column; gap: 10px; }
.docs-card-title { font-size: 14px; font-weight: 700; color: var(--bp-navy); }
.docs-card-meta { font-family: var(--bp-mono); font-size: 11px; color: var(--bp-muted); }
.docs-card-body { font-size: 12px; line-height: 1.5; color: var(--bp-navy); }
.docs-card-actions { padding-top: 8px; border-top: 1px solid color-mix(in srgb, var(--bp-navy) 20%, transparent); }
@media (max-width: 700px) {
.docs-split { grid-template-columns: 1fr; }
.docs-rows { max-height: 50vh; }
}
+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>
+1 -1
View File
@@ -18,7 +18,7 @@ import { buildLiveScenariosFromApi } from "../lib/buildScenarios";
import { api, type ApiCall, type Actor } from "../lib/api";
import type { ProcessScenario } from "../data/types";
export type SceneId = "landing" | "mission" | "history" | "studio" | "settings" | "login" | "sso-callback" | "chat" | "agent" | "hub-procurement" | "hub-hr" | "hub-it" | "geo-attendance" | "explainer" | "approvals";
export type SceneId = "landing" | "mission" | "history" | "studio" | "settings" | "login" | "sso-callback" | "chat" | "agent" | "hub-procurement" | "hub-hr" | "hub-it" | "geo-attendance" | "explainer" | "approvals" | "documents";
export type DataMode = "snapshot" | "live";
export type Theme = "dark" | "light";