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:
@@ -14,6 +14,7 @@ import Hub from "./scenes/Hub";
|
|||||||
import GeoAttendance from "./scenes/GeoAttendance";
|
import GeoAttendance from "./scenes/GeoAttendance";
|
||||||
import Explainer from "./scenes/Explainer";
|
import Explainer from "./scenes/Explainer";
|
||||||
import Approvals from "./scenes/Approvals";
|
import Approvals from "./scenes/Approvals";
|
||||||
|
import Documents from "./scenes/Documents";
|
||||||
import CommandBar from "./components/CommandBar";
|
import CommandBar from "./components/CommandBar";
|
||||||
import Toaster from "./components/Toaster";
|
import Toaster from "./components/Toaster";
|
||||||
import Console from "./components/Console";
|
import Console from "./components/Console";
|
||||||
@@ -169,6 +170,7 @@ export default function App() {
|
|||||||
{scene === "geo-attendance" && <GeoAttendance />}
|
{scene === "geo-attendance" && <GeoAttendance />}
|
||||||
{scene === "explainer" && <Explainer />}
|
{scene === "explainer" && <Explainer />}
|
||||||
{scene === "approvals" && <Approvals />}
|
{scene === "approvals" && <Approvals />}
|
||||||
|
{scene === "documents" && <Documents />}
|
||||||
{scene === "settings" && <Settings />}
|
{scene === "settings" && <Settings />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2088,3 +2088,36 @@ select.studio-input { background: var(--bp-paper); }
|
|||||||
/* Generic scene container padding. */
|
/* Generic scene container padding. */
|
||||||
.hub-scene, .approvals-scene, .geo-scene, .explainer-scene { padding: 16px 12px; }
|
.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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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("agent")}>Command Assistant</button>
|
||||||
<button className="hub-chip" onClick={() => setScene("chat")}>Team Chat</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("approvals")}>Approvals queue</button>
|
||||||
|
<button className="hub-chip" onClick={() => setScene("documents")}>Documents</button>
|
||||||
<button className="hub-chip" onClick={() => setScene("explainer")}>What is FlowMaster?</button>
|
<button className="hub-chip" onClick={() => setScene("explainer")}>What is FlowMaster?</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import { buildLiveScenariosFromApi } from "../lib/buildScenarios";
|
|||||||
import { api, type ApiCall, type Actor } from "../lib/api";
|
import { api, type ApiCall, type Actor } from "../lib/api";
|
||||||
import type { ProcessScenario } from "../data/types";
|
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 DataMode = "snapshot" | "live";
|
||||||
export type Theme = "dark" | "light";
|
export type Theme = "dark" | "light";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user