Oracle round-7 remediation, items 2 + 7:
- flowCuration.NON_BUSINESS_SOURCE_CONTEXTS no longer lists
EA2_DRAFT_PROCESS:process_creation (it's in STARTABLE_SOURCE_CONTEXTS).
Adds CANVAS_AGENT_MEMORY + CANVAS_AGENT_VAULT_ROOT to NON_BUSINESS so
vault docs never leak into hubs / catalogue.
- wizardApi.createView now accepts a fields list and emits proper field
defs (name slug, label, type from {string,number,boolean,textarea}).
- Wizard.handleDataSave rebuilds every step's view with the user's
draft.fields after Generate phase, then rewires the presentation
edges to the new views. Steps now expose real business fields, not a
generic 'Notes' textarea.
445 lines
17 KiB
TypeScript
445 lines
17 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useApp } from "../state/store";
|
|
import { wizardApi, type BatchOperation } from "../lib/wizardApi";
|
|
import { Branch, Check, Close, ChevronRight } from "../components/icons";
|
|
|
|
type WizardStep = "Intake" | "Analyze" | "Generate" | "Validate" | "Draft saved" | "Publish";
|
|
|
|
const STEPS: WizardStep[] = ["Intake", "Analyze", "Generate", "Validate", "Draft saved", "Publish"];
|
|
|
|
interface DraftState {
|
|
step: WizardStep;
|
|
flowKey?: string;
|
|
name: string;
|
|
description: string;
|
|
nodes: any[];
|
|
edges: any[];
|
|
fields: { name: string; type: string }[];
|
|
rules: string[];
|
|
}
|
|
|
|
export default function Wizard() {
|
|
const mode = useApp((s) => s.mode);
|
|
const pushToast = useApp((s) => s.pushToast);
|
|
const refreshLive = useApp((s) => s.refreshLive);
|
|
const actor = useApp((s) => s.actor);
|
|
const setScene = useApp((s) => s.setScene);
|
|
|
|
const canPublish = mode === "live" && !!actor?.user_id;
|
|
|
|
const [draft, setDraft] = useState<DraftState>(() => {
|
|
try {
|
|
const stored = localStorage.getItem("fm.mc.wizard.draft");
|
|
if (stored) return JSON.parse(stored);
|
|
} catch {}
|
|
return {
|
|
step: "Intake",
|
|
name: "",
|
|
description: "",
|
|
nodes: [],
|
|
edges: [],
|
|
fields: [],
|
|
rules: []
|
|
};
|
|
});
|
|
|
|
const [working, setWorking] = useState(false);
|
|
const [publishedKey, setPublishedKey] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem("fm.mc.wizard.draft", JSON.stringify(draft));
|
|
}, [draft]);
|
|
|
|
const updateDraft = (patch: Partial<DraftState>) => {
|
|
setDraft(d => ({ ...d, ...patch }));
|
|
};
|
|
|
|
const handleIntakeSubmit = async () => {
|
|
if (!draft.description) return;
|
|
setWorking(true);
|
|
try {
|
|
const slug = `process_${Date.now()}`;
|
|
const res = await wizardApi.createDraft({
|
|
name: slug,
|
|
display_name: draft.name || "Untitled Process",
|
|
description: draft.description,
|
|
source_context: "EA2_DRAFT_PROCESS:process_creation",
|
|
config: {
|
|
wizard: {
|
|
marker: "EA2_DRAFT_PROCESS",
|
|
maxDepth: 5,
|
|
maxNodes: 40,
|
|
chat: [],
|
|
uploads: [],
|
|
panelState: {},
|
|
debug: []
|
|
}
|
|
}
|
|
});
|
|
|
|
// Auto-generate basic 5-step template
|
|
const templateNodes = [
|
|
{ _key: "n1", kind: "step", display_name: "Capture request details", dispatch_kind: "human" },
|
|
{ _key: "n2", kind: "step", display_name: "Validate requirements", dispatch_kind: "agent", agent_capability: "evaluate_business_rule" },
|
|
{ _key: "n3", kind: "step", display_name: "Prepare work package", dispatch_kind: "agent", agent_capability: "process_design_assistant" },
|
|
{ _key: "n4", kind: "step", display_name: "Review and approve", dispatch_kind: "human" },
|
|
{ _key: "n5", kind: "step", display_name: "Close out process", dispatch_kind: "human" }
|
|
];
|
|
|
|
updateDraft({
|
|
flowKey: res._key,
|
|
step: "Analyze",
|
|
nodes: templateNodes
|
|
});
|
|
pushToast("ok", "Draft created in EA2");
|
|
} catch (err: any) {
|
|
pushToast("err", `Failed to create draft: ${err.message}`);
|
|
} finally {
|
|
setWorking(false);
|
|
}
|
|
};
|
|
|
|
const handleStructureSave = async () => {
|
|
if (!draft.flowKey || !actor) return;
|
|
setWorking(true);
|
|
try {
|
|
const stepCreateOps: BatchOperation[] = draft.nodes.map(n =>
|
|
wizardApi.ops.createStep({
|
|
display_name: n.display_name,
|
|
dispatch_kind: n.dispatch_kind,
|
|
agent_capability: n.agent_capability,
|
|
})
|
|
);
|
|
const viewCreateOps: BatchOperation[] = draft.nodes.map(n =>
|
|
wizardApi.ops.createView({ display_name: n.display_name })
|
|
);
|
|
const versionCreateOp: BatchOperation = wizardApi.ops.createVersion(draft.name || "Process");
|
|
const allCreateOps = [...stepCreateOps, ...viewCreateOps, versionCreateOp];
|
|
|
|
const createRes = await wizardApi.applyBatch(draft.flowKey, allCreateOps, actor);
|
|
const createdOps: any[] = createRes?.result?.ops || [];
|
|
const stepKeys = createdOps.slice(0, draft.nodes.length).map(o => o.key);
|
|
const viewKeys = createdOps.slice(draft.nodes.length, draft.nodes.length * 2).map(o => o.key);
|
|
const versionKey = createdOps[createdOps.length - 1]?.key;
|
|
const nodesWithKeys = draft.nodes.map((n, i) => ({ ...n, _key: stepKeys[i] || n._key, viewKey: viewKeys[i] }));
|
|
|
|
const wireOps: BatchOperation[] = [
|
|
...nodesWithKeys.map((n, _i) => wizardApi.ops.childEdge(draft.flowKey!, n._key)),
|
|
...nodesWithKeys.map(n => wizardApi.ops.dispatchEdge(n._key)),
|
|
...nodesWithKeys.map(n => wizardApi.ops.presentationEdge(n._key, n.viewKey)),
|
|
];
|
|
if (versionKey) wireOps.push(wizardApi.ops.governsEdge(versionKey, draft.flowKey!));
|
|
if (wireOps.length) await wizardApi.applyBatch(draft.flowKey, wireOps, actor);
|
|
|
|
updateDraft({ step: "Generate", nodes: nodesWithKeys });
|
|
pushToast("ok", `Saved ${stepKeys.length} steps with views and version to EA2`);
|
|
} catch (err: any) {
|
|
pushToast("err", `Save failed: ${err.message}`);
|
|
} finally {
|
|
setWorking(false);
|
|
}
|
|
};
|
|
|
|
const handleDataSave = async () => {
|
|
if (!draft.flowKey || !actor) return;
|
|
setWorking(true);
|
|
try {
|
|
await wizardApi.updateDraftConfig(draft.flowKey, {
|
|
config: { wizard: { panelState: { fields: draft.fields } } }
|
|
});
|
|
|
|
if (draft.fields.length > 0 && draft.nodes.length > 0) {
|
|
const replaceOps: BatchOperation[] = draft.nodes.map(n =>
|
|
wizardApi.ops.createView({ display_name: n.display_name, fields: draft.fields })
|
|
);
|
|
const res = await wizardApi.applyBatch(draft.flowKey, replaceOps, actor);
|
|
const newViewKeys = (res?.result?.ops || []).map((o: any) => o.key);
|
|
const wireOps: BatchOperation[] = draft.nodes.map((n, i) =>
|
|
wizardApi.ops.presentationEdge(n._key, newViewKeys[i])
|
|
);
|
|
if (wireOps.length) await wizardApi.applyBatch(draft.flowKey, wireOps, actor);
|
|
updateDraft({ nodes: draft.nodes.map((n, i) => ({ ...n, viewKey: newViewKeys[i] })) });
|
|
}
|
|
|
|
updateDraft({ step: "Validate" });
|
|
pushToast("ok", `Saved ${draft.fields.length} fields onto every step's form`);
|
|
} catch (err: any) {
|
|
pushToast("err", `Save failed: ${err.message}`);
|
|
} finally {
|
|
setWorking(false);
|
|
}
|
|
};
|
|
|
|
const handleRulesSave = async () => {
|
|
if (!draft.flowKey || !actor) return;
|
|
setWorking(true);
|
|
try {
|
|
await wizardApi.updateDraftConfig(draft.flowKey, {
|
|
config: { wizard: { panelState: { rules: draft.rules } } }
|
|
});
|
|
updateDraft({ step: "Draft saved" });
|
|
pushToast("ok", "Rules saved");
|
|
} catch (err: any) {
|
|
pushToast("err", `Save failed: ${err.message}`);
|
|
} finally {
|
|
setWorking(false);
|
|
}
|
|
};
|
|
|
|
const handlePublish = async () => {
|
|
if (!draft.flowKey || !actor) return;
|
|
setWorking(true);
|
|
try {
|
|
const ops: BatchOperation[] = [wizardApi.ops.publishFlow(draft.flowKey)];
|
|
await wizardApi.applyBatch(draft.flowKey, ops, actor);
|
|
updateDraft({ step: "Publish" });
|
|
setPublishedKey(draft.flowKey);
|
|
pushToast("ok", "Process published successfully!");
|
|
refreshLive();
|
|
} catch (err: any) {
|
|
pushToast("err", `Publish failed: ${err.message}`);
|
|
} finally {
|
|
setWorking(false);
|
|
}
|
|
};
|
|
|
|
const handleStartInstance = async () => {
|
|
if(!publishedKey) return;
|
|
setWorking(true);
|
|
try {
|
|
await wizardApi.startInstance(publishedKey);
|
|
pushToast("ok", "New instance started!");
|
|
setScene("mission");
|
|
} catch(err:any) {
|
|
pushToast("err", `Failed to start: ${err.message}`);
|
|
} finally {
|
|
setWorking(false);
|
|
}
|
|
}
|
|
|
|
const renderStepNav = () => (
|
|
<div className="wizard-progress">
|
|
{STEPS.map((step, i) => {
|
|
const isActive = step === draft.step;
|
|
const isPast = STEPS.indexOf(step) < STEPS.indexOf(draft.step);
|
|
return (
|
|
<div key={step} className={`wizard-step-marker ${isActive ? 'active' : ''} ${isPast ? 'past' : ''}`}>
|
|
<span className="step-num">{i + 1}</span>
|
|
<span className="step-label">{step}</span>
|
|
{i < STEPS.length - 1 && <ChevronRight size={12} className="step-chevron" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="wizard-container">
|
|
<header className="wizard-header">
|
|
<div>
|
|
<div className="mc-hero-eyebrow"><Branch size={12} /> Process Creation Wizard</div>
|
|
<h2 className="mc-hero-title">Build a new process</h2>
|
|
</div>
|
|
{renderStepNav()}
|
|
</header>
|
|
|
|
<div className="wizard-content">
|
|
{draft.step === "Intake" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">What process do you want to build?</h3>
|
|
<div className="field-group">
|
|
<label>Name (optional)</label>
|
|
<input
|
|
type="text"
|
|
value={draft.name}
|
|
onChange={e => updateDraft({ name: e.target.value })}
|
|
placeholder="e.g. Laptop Procurement"
|
|
/>
|
|
</div>
|
|
<div className="field-group">
|
|
<label>Describe the process</label>
|
|
<textarea
|
|
value={draft.description}
|
|
onChange={e => updateDraft({ description: e.target.value })}
|
|
placeholder="When a store manager requests a new laptop, run a procurement approval..."
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleIntakeSubmit}
|
|
disabled={!draft.description || working || !canPublish}
|
|
title="POST /api/ea2/flow"
|
|
>
|
|
{working ? 'Saving...' : 'Start Drafting →'}
|
|
</button>
|
|
{!canPublish && <p className="warning-text">Switch to LIVE mode + sign in to create processes.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Analyze" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">Review proposed structure</h3>
|
|
<div className="node-list">
|
|
{draft.nodes.map((n, i) => (
|
|
<div key={n._key} className="node-card">
|
|
<div className="node-header">
|
|
<span className="node-idx">{i + 1}</span>
|
|
<input
|
|
value={n.display_name}
|
|
onChange={e => {
|
|
const newNodes = [...draft.nodes];
|
|
newNodes[i].display_name = e.target.value;
|
|
updateDraft({ nodes: newNodes });
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="node-meta">
|
|
<select
|
|
value={n.dispatch_kind}
|
|
onChange={e => {
|
|
const newNodes = [...draft.nodes];
|
|
newNodes[i].dispatch_kind = e.target.value;
|
|
updateDraft({ nodes: newNodes });
|
|
}}
|
|
>
|
|
<option value="human">Human Task</option>
|
|
<option value="agent">Agent Task</option>
|
|
<option value="system">System Task</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleStructureSave}
|
|
disabled={working}
|
|
title="POST /api/ea2/apply-batch"
|
|
>
|
|
{working ? 'Saving...' : 'Confirm Structure →'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Generate" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">What information do you need to collect?</h3>
|
|
<div className="field-list">
|
|
{draft.fields.map((f, i) => (
|
|
<div key={i} className="field-row">
|
|
<input
|
|
placeholder="Field name"
|
|
value={f.name}
|
|
onChange={e => {
|
|
const newFields = [...draft.fields];
|
|
newFields[i].name = e.target.value;
|
|
updateDraft({ fields: newFields });
|
|
}}
|
|
/>
|
|
<select
|
|
value={f.type}
|
|
onChange={e => {
|
|
const newFields = [...draft.fields];
|
|
newFields[i].type = e.target.value;
|
|
updateDraft({ fields: newFields });
|
|
}}
|
|
>
|
|
<option value="string">Text</option>
|
|
<option value="number">Number</option>
|
|
<option value="boolean">Yes/No</option>
|
|
</select>
|
|
<button className="icon-btn" onClick={() => updateDraft({ fields: draft.fields.filter((_, idx) => idx !== i) })}><Close size={12} /></button>
|
|
</div>
|
|
))}
|
|
<button className="btn btn-secondary" onClick={() => updateDraft({ fields: [...draft.fields, { name: "", type: "string" }] })}>+ Add Field</button>
|
|
</div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleDataSave}
|
|
disabled={working}
|
|
title="PUT /api/ea2/flow/{key}"
|
|
>
|
|
{working ? 'Saving...' : 'Confirm Data →'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Validate" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">When should this process do something different? (Rules)</h3>
|
|
<div className="field-list">
|
|
{draft.rules.map((r, i) => (
|
|
<div key={i} className="field-row">
|
|
<input
|
|
placeholder="e.g. If cost > 5000, require VP approval"
|
|
value={r}
|
|
onChange={e => {
|
|
const newRules = [...draft.rules];
|
|
newRules[i] = e.target.value;
|
|
updateDraft({ rules: newRules });
|
|
}}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<button className="icon-btn" onClick={() => updateDraft({ rules: draft.rules.filter((_, idx) => idx !== i) })}><Close size={12} /></button>
|
|
</div>
|
|
))}
|
|
<button className="btn btn-secondary" onClick={() => updateDraft({ rules: [...draft.rules, ""] })}>+ Add Rule</button>
|
|
</div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleRulesSave}
|
|
disabled={working}
|
|
title="PUT /api/ea2/flow/{key}"
|
|
>
|
|
{working ? 'Saving...' : 'Review Process →'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Draft saved" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">Review & Publish</h3>
|
|
<p>Your process <strong>{draft.name || 'Untitled'}</strong> is ready to be published to EA2.</p>
|
|
<div className="summary-stats">
|
|
<div><span>Steps:</span> {draft.nodes.length}</div>
|
|
<div><span>Data fields:</span> {draft.fields.length}</div>
|
|
<div><span>Rules:</span> {draft.rules.length}</div>
|
|
</div>
|
|
<div className="actions-row">
|
|
<button className="btn btn-secondary" onClick={() => updateDraft({ step: "Intake" })}>Edit</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handlePublish}
|
|
disabled={working}
|
|
title="POST /api/ea2/apply-batch"
|
|
>
|
|
{working ? 'Publishing...' : 'Publish to EA2'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Publish" && (
|
|
<div className="wizard-panel success-panel">
|
|
<div className="success-icon"><Check size={32} /></div>
|
|
<h3 className="panel-h">Process Published!</h3>
|
|
<p>Your process <strong>{draft.name || "Untitled"}</strong> is now active and ready to run.</p>
|
|
<div className="actions-row">
|
|
<button className="btn btn-secondary" onClick={() => { setDraft({ step: "Intake", name: "", description: "", nodes: [], edges: [], fields: [], rules: [] }); setPublishedKey(null); }}>Create Another</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleStartInstance}
|
|
disabled={working}
|
|
title="POST /api/runtime/transactions"
|
|
>
|
|
Start First Instance Now
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|