diff --git a/src/App.tsx b/src/App.tsx index 440b2d2..382940f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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" && } {scene === "explainer" && } {scene === "approvals" && } + {scene === "documents" && } {scene === "settings" && } diff --git a/src/index.css b/src/index.css index 464e9e6..c8c4bf9 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } +} diff --git a/src/scenes/Documents.tsx b/src/scenes/Documents.tsx new file mode 100644 index 0000000..565f2a8 --- /dev/null +++ b/src/scenes/Documents.tsx @@ -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 { + 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(); + 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(null); + const [filter, setFilter] = useState(""); + const [activeKey, setActiveKey] = useState(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 ( +
+
+
Documents
+

Everything your processes capture

+

+ 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. +

+ setFilter(e.target.value)} + /> +
+ +
+
+
DOCUMENTS · {visible.length}
+ {docs === null &&
Loading from EA2…
} + {docs !== null && visible.length === 0 && ( +
+ No documents match. Open the to publish a new flow. +
+ )} +
    + {visible.slice(0, 100).map((d) => ( +
  • + +
  • + ))} +
+
+ +
+
DETAIL
+ {!active &&
Pick a document on the left to read its description and source process.
} + {active && ( +
+
{active.label}
+
From process · {active.flow_label}
+ {active.description &&

{active.description}

} +
+ +
+
+ )} +
+
+
+ ); +} diff --git a/src/scenes/Landing.tsx b/src/scenes/Landing.tsx index 4b74a05..b52352a 100644 --- a/src/scenes/Landing.tsx +++ b/src/scenes/Landing.tsx @@ -85,6 +85,7 @@ export default function Landing() { + diff --git a/src/state/store.ts b/src/state/store.ts index 1082706..4400bdd 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -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";