三面拆分⑬:流程包纯编排化 —— 编排与传递契约归包,实现方式归库,激活看双判据
模型(用户定):流程包只决定活动编排 + 活动传递的契约,不决定活动的实现方式; 节点只分活动点与审批点;图只定名称与顺序,属性在卡上填,链接好实现即可运行; 激活 = ①Mastra 式图编排闭合完善 + ②活动装载,两面判据。 schema(库经外科手术清空,无历史包袱,网关彻底出局): - contracts:删 gateway 节点变体 / GATEWAY_KINDS / GATEWAY_KIND_LABEL / 'gateway-deprecated' 违规项 —— 节点四种:start/end(结构件)+ activity/approval - routing:joinReady 整个删除(多入边即异或汇合,没有「等回齐」;AND 待并行组); selectEdges 精简;closure 删 checkGateway;process-service 删汇合等待与网关直传, 节点一律只收装配后载荷(verdict/outcome 判据由系统注入,不走映射) 编排面(画布 = 名称 + 顺序,其余在属性卡): - 元素盘五件:活动点(单一,不再分标准/案例 —— 那是实现方式)/审批点/起止/卡点 - 属性卡:节点类型二分(活动点/审批点);新增「链接活动实现」选择器(装载面, FlowNode.act 经 merge 成为语义 activityId);范式/版本降为只读装载信息 - 边属性可填:条件(S-FEEL)+ 映射(上游输出口/起单载荷/常量三种来源; workflow-var/expression 归系统隐性,不进设计者面)—— FlowEdge.condition/mapping 工作副本,pkg-to-graph 回填、graph-to-pkg 合并,清空条件即变默认兜底边 - 异常看板 → 激活双判据看板:图编排(结构+属性+契约)与活动装载(unknown-activity) 分面亮灯,两面清零激活才亮 - pkg-to-graph 去 paradigms 绿化与网关分支;图例/文案全线「活动点/审批点」 测试:closure 27(joinReady 两组 describe 随机制摘除)、advance-order 按真图形状重写 (去 as never 捏造的网关节点)、graph-to-pkg 9(新增 act 合并 + 边契约合并/清空用例)、 seed 8、端到端八例真库全绿;五闸退出码验过(api:check 提交后转绿)。 库:purge 后重种,唯一 unified-quote@v1 生效(10 节点 14 边,双面判据齐)。 债:M5-5 清账(兼容路径连 schema 一起摘);M5-4(并行组)仍挂。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -562,6 +562,11 @@
|
||||
.g-ex { cursor: pointer; border-left-color: #b0413e; }
|
||||
.g-ex > b { font-size: 11px; }
|
||||
.g-ex .msg { flex: 1; min-width: 0; font-size: 10.5px; line-height: 1.55; letter-spacing: 0; text-transform: none; color: #3a4452; }
|
||||
.g-exfacet { display: flex; align-items: center; gap: 6px; padding: 6px 2px 2px; font: 600 9.5px/1 var(--mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sh-steel, #46586a); }
|
||||
.g-exfacet.ok { color: #2e7d4f; }
|
||||
.g-maprow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.g-maprow .g-input, .g-maprow .g-select { flex: 1 1 90px; min-width: 70px; }
|
||||
.g-maparr { color: var(--sh-steel, #46586a); font-weight: 700; }
|
||||
.g-exboard-empty { padding: 16px 18px; font-family: var(--sh-mono); font-size: 10px; line-height: 1.9; color: var(--sh-muted); }
|
||||
|
||||
/* 异常板高度随内容走,不被右侧属性卡拉高 —— 三条异常配 900px 空板才是脏 */
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* · 一条流恰一个生效版本;可把生效切回旧版,也可全部停用让整条流下线
|
||||
* · 在途工作流按启动时钉住的版本跑完 —— 启停对它们零影响
|
||||
*/
|
||||
import { useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
||||
import { processPackageSchema, saveProcessRoute, setProcessStateRoute, validateProcessRoute } from '@contracts';
|
||||
@@ -39,7 +39,7 @@ import type { GovActivities } from '@web-shared/gov/gov-api.ts';
|
||||
import { FlowFigureBlock } from '@web-shared/gov/flow/FlowFigure.tsx';
|
||||
import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts';
|
||||
import { mergeGraphIntoPackage } from '@web-shared/gov/flow/graph-to-pkg.ts';
|
||||
import type { FlowGraph, FlowNode } from '@web-shared/gov/flow/flow-layout.ts';
|
||||
import type { FlowEdge, FlowGraph, FlowNode } from '@web-shared/gov/flow/flow-layout.ts';
|
||||
import type { SpPosition } from '@web-shared/gov/flow/support.tsx';
|
||||
import { TriggerRulesHome } from '@features/gov-trigger';
|
||||
|
||||
@@ -66,6 +66,151 @@ function flowsOf(packages: readonly ProcessPackage[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 边属性:传递契约(条件 + 映射)──────────────────────────────────── */
|
||||
|
||||
/** 卡点 id → 去向节点 id(边属性按语义对端认亲:A5→c_reject 即 A5→A4) */
|
||||
function calloutToOf(graph: FlowGraph | null): Map<string, string> | undefined {
|
||||
if (graph === null) return undefined;
|
||||
return new Map(
|
||||
graph.nodes.filter((n) => n.kind === 'callout' && n.to !== undefined).map((n) => [n.id, n.to!]),
|
||||
);
|
||||
}
|
||||
|
||||
/** 映射行的工作形状 —— 语义边 mapping 的元素;设计者只见三种来源,其余是系统隐性上下文 */
|
||||
type MapRow = { target: string; source: Record<string, unknown> & { from: string } };
|
||||
const SOURCE_KINDS = [
|
||||
{ k: 'output-port', label: '上游输出口' },
|
||||
{ k: 'initiation', label: '起单载荷' },
|
||||
{ k: 'constant', label: '常量' },
|
||||
] as const;
|
||||
|
||||
function EdgeProps({
|
||||
edge,
|
||||
toSem,
|
||||
onPatch,
|
||||
}: {
|
||||
edge: FlowEdge;
|
||||
toSem: string;
|
||||
onPatch: (f: (e: FlowEdge) => FlowEdge) => void;
|
||||
}) {
|
||||
const rows = (edge.mapping ?? []) as MapRow[];
|
||||
const setRows = (next: MapRow[]) => onPatch((e) => ({ ...e, mapping: next }));
|
||||
const patchRow = (i: number, f: (r: MapRow) => MapRow) => setRows(rows.map((r, j) => (j === i ? f(r) : r)));
|
||||
return (
|
||||
<div className="g-props">
|
||||
<Prop ico="git-branch" title={`条件 · ${edge.from} → ${toSem}`} extra="多出边异或:命中即走;留空 = 默认兜底">
|
||||
<Field k="S-FEEL 条件" hint="如 needSourcing = true;可用 and / or 连接。留空即无条件(默认边,至多一条)">
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={edge.condition ?? ''}
|
||||
placeholder="留空 = 默认兜底"
|
||||
onChange={(e) => onPatch((x) => ({ ...x, condition: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
<Field k="边标签" hint="画在线旁的注记,辅助阅读,不参与判断">
|
||||
<input
|
||||
className="g-input"
|
||||
value={edge.label ?? ''}
|
||||
placeholder="如:需要选型"
|
||||
onChange={(e) =>
|
||||
onPatch((x) => {
|
||||
const { label: _drop, ...rest } = x;
|
||||
return e.target.value === '' ? rest : { ...rest, label: e.target.value };
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</Prop>
|
||||
<Prop ico="git-merge" title="映射 · 载荷装配" extra="下游字段 ← 来源;必填映射齐了这条边才闭合">
|
||||
{rows.map((r, i) => {
|
||||
const known = SOURCE_KINDS.some((k) => k.k === r.source.from);
|
||||
return (
|
||||
<div className="g-maprow" key={i}>
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={r.target}
|
||||
placeholder="下游字段"
|
||||
onChange={(e) => patchRow(i, (x) => ({ ...x, target: e.target.value }))}
|
||||
/>
|
||||
<span className="g-maparr">←</span>
|
||||
{known ? (
|
||||
<select
|
||||
className="g-select"
|
||||
value={r.source.from}
|
||||
onChange={(e) =>
|
||||
patchRow(i, (x) => ({
|
||||
...x,
|
||||
source:
|
||||
e.target.value === 'output-port'
|
||||
? { from: 'output-port', port: '', path: '' }
|
||||
: e.target.value === 'initiation'
|
||||
? { from: 'initiation', path: '' }
|
||||
: { from: 'constant', value: '' },
|
||||
}))
|
||||
}
|
||||
>
|
||||
{SOURCE_KINDS.map((k) => (
|
||||
<option key={k.k} value={k.k}>
|
||||
{k.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
// workflow-var / expression 等是系统侧来源 —— 保留展示,不给编辑入口
|
||||
<Chip tone="mute">{String(r.source.from)}(系统)</Chip>
|
||||
)}
|
||||
{r.source.from === 'output-port' && (
|
||||
<>
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={String(r.source['port'] ?? '')}
|
||||
placeholder="口名"
|
||||
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, port: e.target.value } }))}
|
||||
/>
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={String(r.source['path'] ?? '')}
|
||||
placeholder="字段路径"
|
||||
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, path: e.target.value } }))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{r.source.from === 'initiation' && (
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={String(r.source['path'] ?? '')}
|
||||
placeholder="起单字段路径"
|
||||
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, path: e.target.value } }))}
|
||||
/>
|
||||
)}
|
||||
{r.source.from === 'constant' && (
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={String(r.source['value'] ?? '')}
|
||||
placeholder="常量值"
|
||||
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, value: e.target.value } }))}
|
||||
/>
|
||||
)}
|
||||
<button className="g-btn" onClick={() => setRows(rows.filter((_, j) => j !== i))}>
|
||||
<Ico name="x" s={12} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Acts
|
||||
items={[
|
||||
{
|
||||
label: '加一条映射',
|
||||
ico: 'plus',
|
||||
onClick: () => setRows([...rows, { target: '', source: { from: 'output-port', port: '', path: '' } }]),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Prop>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 台面:总览 · 流程包 · 触发器 ---------------- */
|
||||
|
||||
export function WorkflowsHome({ tab, nav }: HomeProps) {
|
||||
@@ -218,7 +363,6 @@ function FlowShelf({ nav }: { nav: Nav }) {
|
||||
const c = {
|
||||
activity: shown.nodes.filter((n) => n.kind === 'activity').length,
|
||||
approval: shown.nodes.filter((n) => n.kind === 'approval').length,
|
||||
gateway: shown.nodes.filter((n) => n.kind === 'gateway').length,
|
||||
};
|
||||
return (
|
||||
<button
|
||||
@@ -252,7 +396,7 @@ function FlowShelf({ nav }: { nav: Nav }) {
|
||||
审批 {c.approval}
|
||||
</Chip>
|
||||
<Chip tone="mute" ico="git-branch">
|
||||
网关 {c.gateway}
|
||||
边 {shown.edges.length}
|
||||
</Chip>
|
||||
</span>
|
||||
<span className="g-tile-f">
|
||||
@@ -351,11 +495,11 @@ function FlowLab({
|
||||
* 任何版本都可编辑:保存恒存为**未生效**的新版本,谁也伤不着 ——
|
||||
* 在旧版上改就是「以旧版为底另起一版」。生效切换只在激活那一下。
|
||||
*/
|
||||
const paradigms = useMemo(() => new Map(activities.map((a) => [a.activityId, a.paradigm])), [activities]);
|
||||
|
||||
/** 画布工作副本:属性卡与手绘编辑都改它;保存时合并回流程包 */
|
||||
const [graph, setGraph] = useState<FlowGraph | null>(() => packageToGraph(pkg, [], paradigms));
|
||||
const [graph, setGraph] = useState<FlowGraph | null>(() => packageToGraph(pkg, []));
|
||||
const [sel, setSel] = useState<string | null>(null);
|
||||
/** 边选中态:graph.edges 的下标(与节点选中互斥 —— 属性卡一次编辑一件事) */
|
||||
const [selEdge, setSelEdge] = useState<number | null>(null);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
||||
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
||||
@@ -369,7 +513,8 @@ function FlowLab({
|
||||
? undefined
|
||||
: activities.find(
|
||||
(a) =>
|
||||
(semanticNode?.kind === 'activity' && a.activityId === semanticNode.activityId) || a.name === node.label,
|
||||
(node.act !== undefined && a.activityId === node.act) ||
|
||||
(node.act === undefined && semanticNode?.kind === 'activity' && a.activityId === semanticNode.activityId),
|
||||
);
|
||||
|
||||
// 每次进页问一次闭合 —— 画图即校验(与后端裁决同源);落笔保存后用返回的报告
|
||||
@@ -390,28 +535,14 @@ function FlowLab({
|
||||
n.kind === 'activity'
|
||||
? n.flavor === 'approval'
|
||||
? '审批点'
|
||||
: n.flavor === 'case'
|
||||
? '案例活动 · loop'
|
||||
: '标准活动 · flow'
|
||||
: n.kind === 'gateway'
|
||||
? n.mode === 'and'
|
||||
? '并行网关'
|
||||
: '判断网关'
|
||||
: n.kind === 'callout'
|
||||
? '卡点'
|
||||
: n.kind === 'start'
|
||||
? '调用接口'
|
||||
: '显式终点';
|
||||
const kindTone = (n: FlowNode) =>
|
||||
n.kind === 'activity'
|
||||
? n.flavor === 'approval'
|
||||
? 'warn'
|
||||
: n.flavor === 'case'
|
||||
? 'collab'
|
||||
: 'auto'
|
||||
: '活动点'
|
||||
: n.kind === 'callout'
|
||||
? 'danger'
|
||||
: 'mute';
|
||||
? '卡点'
|
||||
: n.kind === 'start'
|
||||
? '调用接口'
|
||||
: '显式终点';
|
||||
const kindTone = (n: FlowNode) =>
|
||||
n.kind === 'activity' ? (n.flavor === 'approval' ? 'warn' : 'auto') : n.kind === 'callout' ? 'danger' : 'mute';
|
||||
|
||||
// 画布上的选中态:diagram-js 给每个形状挂了 data-element-id,外挂一个类,不改画布件
|
||||
useEffect(() => {
|
||||
@@ -694,16 +825,16 @@ function FlowLab({
|
||||
<FlowFigureBlock
|
||||
key={`${pkg.id}@${pkg.version}`}
|
||||
title={`${pkg.name} v${pkg.version}`}
|
||||
oneLiner={`蓝 = 标准活动 · 绿 = 案例活动 · 琥珀 = 审批点 · 红 = 卡点`}
|
||||
oneLiner={`蓝 = 活动点 · 琥珀 = 审批点 · 红 = 卡点`}
|
||||
graph={graph}
|
||||
drilledIds={nodes.filter((n) => n.kind === 'activity').map((n) => n.id)}
|
||||
/*
|
||||
* 画布只画**编排逻辑**:节点与网关的名字、连线;边上的字是辅助阅读的注记。
|
||||
* 画布只画**编排逻辑**:节点的名字、连线的顺序;边上的字是辅助阅读的注记。
|
||||
* 图上真正生效可编辑的只有**名字** —— 不传 positions,双击就不出岗位一栏。
|
||||
* 岗位、判据、映射这些闭环要素在**节点属性**里决定;分配到人是运营的事,这里不碰。
|
||||
*/
|
||||
canEdit
|
||||
onNodeClick={(nid) => setSel(nid)}
|
||||
onNodeClick={(nid) => { setSel(nid); setSelEdge(null); }}
|
||||
onEditNodeText={async (nid, p2) => {
|
||||
setGraph((g) =>
|
||||
g === null
|
||||
@@ -727,39 +858,59 @@ function FlowLab({
|
||||
* 清零了激活按钮才亮 —— 完整性闸在激活,不在画图。
|
||||
*/}
|
||||
<aside className="g-vercard g-exboard">
|
||||
<div className="g-vercard-h">异常 · 看板</div>
|
||||
<div className="g-vercard-h">激活 · 双判据</div>
|
||||
<div className="g-vercard-list">
|
||||
{activeReport === undefined ? (
|
||||
<div className="g-exboard-empty">校验中…</div>
|
||||
) : activeReport.violations.length === 0 ? (
|
||||
<div className="g-exboard-empty">
|
||||
没有异常 —— v{pkg.version} 的编排闭合了。
|
||||
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
|
||||
</div>
|
||||
) : (
|
||||
activeReport.violations.map((v, i) => {
|
||||
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
|
||||
(() => {
|
||||
/* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */
|
||||
const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity');
|
||||
const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity');
|
||||
const row = (v: (typeof activeReport.violations)[number], i: number) => {
|
||||
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
|
||||
return (
|
||||
<div
|
||||
key={`${v.kind}-${i}`}
|
||||
className="g-ver g-ex"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
|
||||
}}
|
||||
>
|
||||
{nid !== undefined && <b>{String(nid)}</b>}
|
||||
<span className="msg">{v.message}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={`${v.kind}-${i}`}
|
||||
className="g-ver g-ex"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (nid !== undefined) setSel(String(nid));
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && nid !== undefined) setSel(String(nid));
|
||||
}}
|
||||
>
|
||||
{nid !== undefined && <b>{String(nid)}</b>}
|
||||
<span className="msg">{v.message}</span>
|
||||
</div>
|
||||
<>
|
||||
<div className={'g-exfacet' + (drawing.length === 0 ? ' ok' : '')}>
|
||||
<Ico name={drawing.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
|
||||
图编排 {drawing.length === 0 ? '闭合' : `${drawing.length} 项待补`}
|
||||
</div>
|
||||
{drawing.map(row)}
|
||||
<div className={'g-exfacet' + (loading.length === 0 ? ' ok' : '')}>
|
||||
<Ico name={loading.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
|
||||
活动装载 {loading.length === 0 ? '齐' : `${loading.length} 个节点未链接实现`}
|
||||
</div>
|
||||
{loading.map(row)}
|
||||
{activeReport.violations.length === 0 && (
|
||||
<div className="g-exboard-empty">
|
||||
两面都齐 —— v{pkg.version} 可运行。
|
||||
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
<div className="g-vercard-f">点一条异常,定位到它的节点属性;清零后才可激活。</div>
|
||||
<div className="g-vercard-f">点一条定位到属性去补;两面清零,激活才亮。</div>
|
||||
</aside>
|
||||
|
||||
{/* 右下:节点属性(图纸卡,与流程图同宽)—— 闭环(岗位/判据/映射)在这里决定 */}
|
||||
@@ -791,16 +942,47 @@ function FlowLab({
|
||||
<button
|
||||
key={n.id}
|
||||
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
|
||||
onClick={() => setSel(sel === n.id ? null : n.id)}
|
||||
onClick={() => { setSel(sel === n.id ? null : n.id); setSelEdge(null); }}
|
||||
>
|
||||
<b className="g-mono">{n.id}</b>
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{node === null ? (
|
||||
<Note>点画布上的节点(或上面的节点条)选一个 —— 属性就在下面填,网关判据与卡点去向同样在此改。</Note>
|
||||
) : (
|
||||
{/* 边拣选条:边属性 = 传递契约(条件 + 映射)—— 图只定顺序,契约在这儿填 */}
|
||||
<div className="g-picker">
|
||||
{(graph?.edges ?? []).map((e, i) => {
|
||||
const toSem = calloutToOf(graph)?.get(e.to) ?? e.to;
|
||||
const brief = e.condition !== undefined && e.condition !== '' ? ` [${e.condition}]` : '';
|
||||
return (
|
||||
<button
|
||||
key={`${e.from}→${e.to}-${i}`}
|
||||
className={'g-pick t-mute' + (selEdge === i ? ' on' : '')}
|
||||
onClick={() => { setSelEdge(selEdge === i ? null : i); setSel(null); }}
|
||||
>
|
||||
<b className="g-mono">{e.from}→{toSem}</b>
|
||||
{(e.label ?? '') + brief}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{node === null && selEdge === null ? (
|
||||
<Note>
|
||||
点画布或上面的拣选条选一件 —— 节点填岗位与链接实现,边填传递契约(条件 + 映射)。
|
||||
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
|
||||
</Note>
|
||||
) : node === null && selEdge !== null && graph !== null ? (
|
||||
<EdgeProps
|
||||
edge={graph.edges[selEdge]!}
|
||||
toSem={calloutToOf(graph)?.get(graph.edges[selEdge]!.to) ?? graph.edges[selEdge]!.to}
|
||||
onPatch={(f) => {
|
||||
setGraph((g) =>
|
||||
g === null ? g : { ...g, edges: g.edges.map((e, i) => (i === selEdge ? f(e) : e)) },
|
||||
);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
) : node === null ? null : (
|
||||
<>
|
||||
<div className="g-props">
|
||||
<Prop ico="tag" title="身份与命名" extra={kindLabel(node)}>
|
||||
@@ -841,34 +1023,21 @@ function FlowLab({
|
||||
}
|
||||
>
|
||||
{node.kind === 'activity' && (
|
||||
<Field k="活动类型" hint="决定它在运行时长成什么">
|
||||
<Field k="节点类型" hint="节点只分两种 —— 实现方式是活动库的事,这里不选">
|
||||
<Seg
|
||||
value={node.flavor ?? 'flow'}
|
||||
value={node.flavor === 'approval' ? 'approval' : 'act'}
|
||||
options={[
|
||||
{ k: 'flow', label: '标准 · flow' },
|
||||
{ k: 'case', label: '案例 · loop' },
|
||||
{ k: 'act', label: '活动点' },
|
||||
{ k: 'approval', label: '审批点' },
|
||||
]}
|
||||
onPick={(k) =>
|
||||
patch((n) => ({ ...n, flavor: k === 'flow' ? undefined : (k as 'case' | 'approval') }))
|
||||
patch((n) => ({ ...n, flavor: k === 'approval' ? ('approval' as const) : undefined }))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{node.kind === 'gateway' && (
|
||||
<Field k="网关模式" hint="× 判断分流 / + 并行同走">
|
||||
<Seg
|
||||
value={node.mode ?? 'x'}
|
||||
options={[
|
||||
{ k: 'x', label: '判断 ×' },
|
||||
{ k: 'and', label: '并行 +' },
|
||||
]}
|
||||
onPick={(k) => patch((n) => ({ ...n, mode: k as 'x' | 'and' }))}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
k={node.kind === 'gateway' ? '判定岗位' : node.flavor === 'approval' ? '审批岗位' : '责任岗位'}
|
||||
k={node.flavor === 'approval' ? '审批岗位' : '责任岗位'}
|
||||
hint={node.flavor === 'approval' ? '审批单派给它' : '任务的负责人由它解析而来'}
|
||||
>
|
||||
<select
|
||||
@@ -919,19 +1088,6 @@ function FlowLab({
|
||||
/>
|
||||
</Prop>
|
||||
)}
|
||||
{node.kind === 'gateway' && (
|
||||
<Prop ico="shield-check" title="判据" extra="条件用 S-FEEL 子集,静态可检">
|
||||
<textarea
|
||||
className="g-ta"
|
||||
rows={3}
|
||||
value={node.basis ?? ''}
|
||||
placeholder="如:台账 90 天内有成交锚点则直接定价,否则先寻源"
|
||||
onChange={(e) =>
|
||||
patch((n) => ({ ...n, basis: e.target.value === '' ? undefined : e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Prop>
|
||||
)}
|
||||
{node.kind === 'callout' && (
|
||||
<Prop ico="rotate-ccw" title="去向" extra="失败走向 / 驳回去向">
|
||||
<Field k="回到节点">
|
||||
@@ -955,15 +1111,29 @@ function FlowLab({
|
||||
</Prop>
|
||||
)}
|
||||
{node.kind === 'activity' && node.flavor !== 'approval' && (
|
||||
<Prop ico="boxes" title="引用的活动" extra="活动住库,编排只引用">
|
||||
<Prop ico="boxes" title="链接活动实现" extra="装载面:编排不含实现,链接决定可运行">
|
||||
<Field k="实现" hint="活动住库;链好了这节点才算装载(激活判据之二)">
|
||||
<select
|
||||
className="g-select"
|
||||
value={refAct?.activityId ?? ''}
|
||||
onChange={(e) =>
|
||||
patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))
|
||||
}
|
||||
>
|
||||
<option value="">未链接 —— 激活会被「活动装载」拦下</option>
|
||||
{activities.map((a) => (
|
||||
<option key={a.activityId} value={a.activityId}>
|
||||
{a.name} · {a.activityId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
{refAct !== undefined ? (
|
||||
<>
|
||||
{/* 实现方式(范式/版本)是装载信息,只读展示 —— 编排面不决定它 */}
|
||||
<div className="g-prop-ref">
|
||||
<Chip
|
||||
tone={refAct.paradigm === 'loop' ? 'collab' : 'auto'}
|
||||
ico={refAct.paradigm === 'loop' ? 'repeat' : 'git-commit'}
|
||||
>
|
||||
{refAct.paradigm === 'loop' ? '案例活动 loop' : '标准活动 flow'}
|
||||
<Chip tone={refAct.paradigm === 'loop' ? 'collab' : 'auto'} ico="git-commit">
|
||||
{refAct.paradigm === 'loop' ? '实现:案例 loop' : '实现:标准 flow'}
|
||||
</Chip>
|
||||
<Chip tone="mute">
|
||||
v{refAct.packageVersions[0]?.version ?? '—'} × a
|
||||
@@ -979,7 +1149,7 @@ function FlowLab({
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Note tone="warn">还没对上活动库里已安装的活动 —— 先去活动库安装;保存时会核对契约相容。</Note>
|
||||
<Note tone="warn">还没链接活动库里已安装的实现 —— 图可以照画照存,激活前链上即可。</Note>
|
||||
)}
|
||||
</Prop>
|
||||
)}
|
||||
|
||||
@@ -102,10 +102,9 @@ export function FlowFigureBlock({
|
||||
</span>
|
||||
<span><i className="lg-i fe-pi-start" aria-hidden /> 起始</span>
|
||||
<span><i className="lg-i fe-pi-end" aria-hidden /> 结束</span>
|
||||
<span><i className="lg-i fe-pi-sop" aria-hidden /> 标准活动</span>
|
||||
<span><i className="lg-i fe-pi-case" aria-hidden /> 案例活动</span>
|
||||
{/* 节点只分活动点/审批点 —— 标准/案例是实现方式,不进编排面的词汇表 */}
|
||||
<span><i className="lg-i fe-pi-sop" aria-hidden /> 活动点</span>
|
||||
<span><i className="lg-i fe-pi-approval" aria-hidden /> 审批点</span>
|
||||
{/* 网关已废除(2026-07-28):分支 = 条件出边,图例不再教它 */}
|
||||
<span><i className="lg-i fe-pi-callout" aria-hidden /> 卡点</span>
|
||||
<span><i className="lg-i lg-flow" aria-hidden /> 顺序流</span>
|
||||
<span className="sep">|</span>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* 画布图 → 流程包:**合并**,不是重建。
|
||||
*
|
||||
* 这里守两条容易丢东西的路:
|
||||
* ① 画布装不下的声明(边映射、驳回走向、负责人策略)保存后必须还在;
|
||||
* ② 回边以**卡点**呈现(MVP 的画法)—— 存盘要还原成语义边,不能连边一起丢。
|
||||
* 这里守三条容易丢东西的路:
|
||||
* ① 画布装不下的声明(驳回走向、负责人策略)保存后必须还在;
|
||||
* ② 回边以**卡点**呈现(MVP 的画法)—— 存盘要还原成语义边,不能连边一起丢;
|
||||
* ③ 属性卡的工作副本(节点的链接实现 act、边的条件/映射)保存时要合并为语义。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { processPackageSchema } from '@contracts';
|
||||
@@ -27,14 +28,22 @@ const basePkg: ProcessPackage = processPackageSchema.parse({
|
||||
owner: { position: 'BD', strategy: 'rotation' },
|
||||
failure: { kind: 'terminate' },
|
||||
},
|
||||
{ id: 'G', kind: 'gateway', label: '岔', gateway: 'exclusive', mode: 'fork' },
|
||||
{
|
||||
id: 'B',
|
||||
kind: 'activity',
|
||||
label: '乙',
|
||||
activityId: 'act-b',
|
||||
owner: { position: 'PM', strategy: 'sole' },
|
||||
failure: { kind: 'terminate' },
|
||||
},
|
||||
{ id: 'E', kind: 'end', label: '终' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-s-a', from: 'S', to: 'A', mapping: [{ target: 'x', source: { from: 'initiation', path: 'x' } }] },
|
||||
{ id: 'e-a-g', from: 'A', to: 'G', mapping: [] },
|
||||
{ id: 'e-g-e', from: 'G', to: 'E', condition: 'ok = true', mapping: [] },
|
||||
{ id: 'e-g-a', from: 'G', to: 'A', condition: 'ok = false', mapping: [], reopen: true },
|
||||
{ id: 'e-a-b', from: 'A', to: 'B', mapping: [] },
|
||||
// 乙的两条互斥出边:通过 → 终点;有异议 → 回甲重开(无网关:分支就在节点身上)
|
||||
{ id: 'e-b-e', from: 'B', to: 'E', condition: 'ok = true', mapping: [] },
|
||||
{ id: 'e-b-a', from: 'B', to: 'A', condition: 'ok = false', mapping: [], reopen: true },
|
||||
],
|
||||
variables: [],
|
||||
});
|
||||
@@ -47,32 +56,32 @@ const graph: FlowGraph = {
|
||||
nodes: [
|
||||
{ id: 'S', kind: 'start', label: '起' },
|
||||
{ id: 'A', kind: 'activity', label: '甲', owner: 'BD' },
|
||||
{ id: 'G', kind: 'gateway', label: '岔', mode: 'x' },
|
||||
{ id: 'B', kind: 'activity', label: '乙', owner: 'PM' },
|
||||
{ id: 'E', kind: 'end', label: '终' },
|
||||
{ id: 'c1', kind: 'callout', label: '返回调整', to: 'A' },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'S', to: 'A' },
|
||||
{ from: 'A', to: 'G' },
|
||||
{ from: 'G', to: 'E', label: '通过', tone: 'ok' },
|
||||
{ from: 'G', to: 'c1', label: '有异议', tone: 'bad' },
|
||||
{ from: 'A', to: 'B' },
|
||||
{ from: 'B', to: 'E', label: '通过', tone: 'ok' },
|
||||
{ from: 'B', to: 'c1', label: '有异议', tone: 'bad' },
|
||||
],
|
||||
};
|
||||
|
||||
describe('mergeGraphIntoPackage', () => {
|
||||
it('卡点还原成语义边 —— 回边不因「画成卡点」而消失', () => {
|
||||
const merged = mergeGraphIntoPackage(basePkg, graph);
|
||||
const back = merged.edges.find((e) => e.from === 'G' && e.to === 'A');
|
||||
const back = merged.edges.find((e) => e.from === 'B' && e.to === 'A');
|
||||
expect(back).toBeDefined();
|
||||
// 旧边的 id 与 reopen 标记跟着保留:合并按 from→to 认亲
|
||||
expect(back?.id).toBe('e-g-a');
|
||||
expect(back?.id).toBe('e-b-a');
|
||||
expect(back?.reopen).toBe(true);
|
||||
expect(back?.condition).toBe('ok = false');
|
||||
});
|
||||
|
||||
it('卡点本身不入语义节点,但整张图存进 diagram', () => {
|
||||
const merged = mergeGraphIntoPackage(basePkg, graph);
|
||||
expect(merged.nodes.map((n) => n.id)).toEqual(['S', 'A', 'G', 'E']);
|
||||
expect(merged.nodes.map((n) => n.id)).toEqual(['S', 'A', 'B', 'E']);
|
||||
const diagram = merged.diagram as { nodes: { id: string }[] };
|
||||
expect(diagram.nodes.map((n) => n.id)).toContain('c1');
|
||||
});
|
||||
@@ -87,7 +96,7 @@ describe('mergeGraphIntoPackage', () => {
|
||||
const merged = mergeGraphIntoPackage(basePkg, withReject);
|
||||
expect(merged.edges.some((e) => e.from === 'AP' && e.to === 'A')).toBe(false);
|
||||
// 旧包里有的回边照旧认得出来
|
||||
expect(merged.edges.some((e) => e.from === 'G' && e.to === 'A')).toBe(true);
|
||||
expect(merged.edges.some((e) => e.from === 'B' && e.to === 'A')).toBe(true);
|
||||
});
|
||||
|
||||
it('画布装不下的声明原样保留:边映射与负责人策略', () => {
|
||||
@@ -96,4 +105,40 @@ describe('mergeGraphIntoPackage', () => {
|
||||
const a = merged.nodes.find((n) => n.id === 'A');
|
||||
expect(a?.kind === 'activity' && a.owner.strategy).toBe('rotation');
|
||||
});
|
||||
|
||||
/* ── 属性卡工作副本的合并(2026-07-28 第二轮:节点链接实现、边传递契约) ── */
|
||||
|
||||
it('节点的 act(链接的活动实现)优先于旧声明成为 activityId', () => {
|
||||
const linked: FlowGraph = {
|
||||
...graph,
|
||||
nodes: graph.nodes.map((n) => (n.id === 'A' ? { ...n, act: 'act-new' } : n)),
|
||||
};
|
||||
const merged = mergeGraphIntoPackage(basePkg, linked);
|
||||
const a = merged.nodes.find((n) => n.id === 'A');
|
||||
expect(a?.kind === 'activity' && a.activityId).toBe('act-new');
|
||||
// 没动过的节点沿用旧声明
|
||||
const b = merged.nodes.find((n) => n.id === 'B');
|
||||
expect(b?.kind === 'activity' && b.activityId).toBe('act-b');
|
||||
});
|
||||
|
||||
it('边的条件/映射工作副本优先;清空条件 = 变默认兜底边', () => {
|
||||
const edited: FlowGraph = {
|
||||
...graph,
|
||||
edges: graph.edges.map((e) =>
|
||||
e.from === 'B' && e.to === 'E'
|
||||
? { ...e, condition: 'ok = true and sum > 3', mapping: [{ target: 'y', source: { from: 'initiation', path: 'y' } }] }
|
||||
: e.from === 'B' && e.to === 'c1'
|
||||
? { ...e, condition: '' } // 设计者把条件清空:这条边成了默认兜底
|
||||
: e,
|
||||
),
|
||||
};
|
||||
const merged = mergeGraphIntoPackage(basePkg, edited);
|
||||
const pass = merged.edges.find((e) => e.id === 'e-b-e');
|
||||
expect(pass?.condition).toBe('ok = true and sum > 3');
|
||||
expect(pass?.mapping).toHaveLength(1);
|
||||
const back = merged.edges.find((e) => e.id === 'e-b-a');
|
||||
expect(back?.condition).toBeUndefined();
|
||||
// 没动过的边沿用旧声明
|
||||
expect(merged.edges.find((e) => e.id === 'e-s-a')?.mapping).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ type Any = any;
|
||||
|
||||
export interface NewNodeSpec {
|
||||
kind: FlowNodeKind;
|
||||
flavor?: "case" | "approval";
|
||||
flavor?: "approval";
|
||||
mode?: "x" | "and";
|
||||
}
|
||||
|
||||
@@ -430,12 +430,12 @@ const modelingModule = {
|
||||
/* ---------- 元素盘(palette):拖放/点击落子 ---------- */
|
||||
|
||||
/*
|
||||
* 网关不再入盘(产品决策 2026-07-28):分支 = 源节点的多条条件出边,汇合 = 多入边异或。
|
||||
* 画布仍**认得** gateway(渲染/序列化都留着)—— 历史版本要能看;只是不给新画。
|
||||
* 节点只分**活动点**与**审批点**(2026-07-28 第二轮):
|
||||
* 编排面不选实现方式(标准/案例是活动库的事,链接哪个实现在属性卡决定);
|
||||
* 网关不存在(分支 = 条件出边);起止与卡点是结构件与呈现件。
|
||||
*/
|
||||
export const PALETTE_SPECS: Array<{ key: string; label: string; className: string; spec: NewNodeSpec }> = [
|
||||
{ key: "create.sop", label: "标准活动 · flow(蓝 · 照谱执行)", className: "fe-pi fe-pi-sop", spec: { kind: "activity" } },
|
||||
{ key: "create.case", label: "案例活动 · loop(绿 · 临场演绎)", className: "fe-pi fe-pi-case", spec: { kind: "activity", flavor: "case" } },
|
||||
{ key: "create.act", label: "活动点(蓝 · 链接活动实现后可运行)", className: "fe-pi fe-pi-sop", spec: { kind: "activity" } },
|
||||
{ key: "create.approval", label: "审批点(琥珀 · 人裁节点)", className: "fe-pi fe-pi-approval", spec: { kind: "activity", flavor: "approval" } },
|
||||
{ key: "create.start", label: "起始事件", className: "fe-pi fe-pi-start", spec: { kind: "start" } },
|
||||
{ key: "create.end", label: "结束事件", className: "fe-pi fe-pi-end", spec: { kind: "end" } },
|
||||
@@ -606,10 +606,8 @@ class FlowContextPad {
|
||||
},
|
||||
};
|
||||
};
|
||||
appendEntry("append.sop", "后接标准活动(自动连线)", "fe-ci fe-pi-sop", { kind: "activity" });
|
||||
appendEntry("append.case", "后接案例活动(自动连线)", "fe-ci fe-pi-case", { kind: "activity", flavor: "case" });
|
||||
appendEntry("append.act", "后接活动点(自动连线)", "fe-ci fe-pi-sop", { kind: "activity" });
|
||||
appendEntry("append.approval", "后接审批点(自动连线)", "fe-ci fe-pi-approval", { kind: "activity", flavor: "approval" });
|
||||
// 网关已废除,不再可追加 —— 分支画成本节点的多条条件出边即可
|
||||
appendEntry("append.end", "后接结束事件(自动连线)", "fe-ci fe-pi-end", { kind: "end" });
|
||||
|
||||
entries["connect"] = {
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface FlowNode {
|
||||
assign?: "auto" | "assist" | "human";
|
||||
/** Wait/等待(BPMN 调用活动):被调流程规范名(软引用);画双边框+等待角词,点击跳被调流程。 */
|
||||
wait?: string;
|
||||
/** 仅活动点:**链接的活动实现 id**(装载面)。属性卡选定,保存时合并为语义 activityId。 */
|
||||
act?: string | undefined;
|
||||
/** 手绘图(graph.manual)专用:节点左上角坐标(px,MANUAL_GRID 的倍数)。 */
|
||||
x?: number;
|
||||
y?: number;
|
||||
@@ -72,6 +74,10 @@ export interface FlowEdge {
|
||||
to: string;
|
||||
label?: string;
|
||||
tone?: FlowTone;
|
||||
/** 边属性(传递契约)的工作副本:S-FEEL 条件。属性卡填写,保存时合并为语义边的 condition。 */
|
||||
condition?: string | undefined;
|
||||
/** 边属性(传递契约)的工作副本:字段映射(形状同语义边 mapping)。保存时合并。 */
|
||||
mapping?: unknown[] | undefined;
|
||||
/** 手绘图专用:整条折线路径(px,含两端;每段水平或垂直)。 */
|
||||
points?: Array<{ x: number; y: number }>;
|
||||
/** 手绘图专用:线标签文字中心点(注记,自由摆放)。 */
|
||||
|
||||
@@ -32,12 +32,9 @@ export function mergeGraphIntoPackage(
|
||||
priorEdge.set(`${from}→${to}`, e);
|
||||
}
|
||||
|
||||
// 画布节点里只有五要素入语义;卡点(callout)留在 diagram
|
||||
// 画布节点里只有活动点/审批点/起终点入语义;卡点(callout)留在 diagram
|
||||
const semantic = graph.nodes.filter((n) => n.kind !== 'callout');
|
||||
|
||||
const degree = (id: string, dir: 'in' | 'out') =>
|
||||
graph.edges.filter((e) => (dir === 'in' ? e.to === id : e.from === id)).length;
|
||||
|
||||
const nodes: ProcessNode[] = semantic.map((n) => {
|
||||
const prior = renamed.get(n.id);
|
||||
const id = nodeIdSchema.parse(n.id);
|
||||
@@ -63,18 +60,6 @@ export function mergeGraphIntoPackage(
|
||||
};
|
||||
}
|
||||
|
||||
if (n.kind === 'gateway') {
|
||||
const priorGw = prior?.kind === 'gateway' ? prior : undefined;
|
||||
return {
|
||||
...base,
|
||||
kind: 'gateway' as const,
|
||||
gateway: (n.mode === 'and' ? 'parallel' : 'exclusive') as 'parallel' | 'exclusive',
|
||||
// 分叉/汇合看度数:出边多为叉,入边多为汇;旧声明优先(度数打平时不猜)
|
||||
mode: priorGw?.mode ?? (degree(n.id, 'out') > degree(n.id, 'in') ? ('fork' as const) : ('join' as const)),
|
||||
...(n.basis === undefined ? {} : { basis: n.basis }),
|
||||
};
|
||||
}
|
||||
|
||||
if (n.flavor === 'approval') {
|
||||
const priorAp = prior?.kind === 'approval' ? prior : undefined;
|
||||
return {
|
||||
@@ -95,7 +80,8 @@ export function mergeGraphIntoPackage(
|
||||
return {
|
||||
...base,
|
||||
kind: 'activity' as const,
|
||||
activityId: priorAct?.activityId ?? placeholderActivityId(n.label),
|
||||
// 链接优先级:属性卡刚选的(act)> 旧声明 > 占位软引用(闭合校验标「未装载」)
|
||||
activityId: n.act !== undefined && n.act !== '' ? activityIdSchema.parse(n.act) : (priorAct?.activityId ?? placeholderActivityId(n.label)),
|
||||
owner: {
|
||||
position: n.owner ?? priorAct?.owner.position ?? '未声明',
|
||||
strategy: priorAct?.owner.strategy ?? ('sole' as const),
|
||||
@@ -135,13 +121,14 @@ export function mergeGraphIntoPackage(
|
||||
.filter((e) => semanticIds.has(e.from) && semanticIds.has(e.to))
|
||||
.map((e) => {
|
||||
const prior = priorEdge.get(`${e.from}→${e.to}`);
|
||||
// 边属性(传递契约)以属性卡的工作副本优先;没动过的沿用旧声明
|
||||
const condition = e.condition !== undefined ? (e.condition.trim() === '' ? undefined : e.condition) : prior?.condition;
|
||||
return {
|
||||
id: prior?.id ?? `e-${e.from}-${e.to}`,
|
||||
from: nodeIdSchema.parse(e.from),
|
||||
to: nodeIdSchema.parse(e.to),
|
||||
// 装配映射与条件是「画布上看不见的声明」—— 原样保留,画布删不掉它们
|
||||
mapping: prior?.mapping ?? [],
|
||||
...(prior?.condition === undefined ? {} : { condition: prior.condition }),
|
||||
mapping: (e.mapping as Edge['mapping'] | undefined) ?? prior?.mapping ?? [],
|
||||
...(condition === undefined ? {} : { condition }),
|
||||
reopen: prior?.reopen ?? false,
|
||||
...(e.label === undefined ? {} : { label: e.label }),
|
||||
...(e.tone === 'ok' || e.tone === 'bad' ? { tone: e.tone } : {}),
|
||||
|
||||
@@ -20,12 +20,7 @@ export type InstanceMark = {
|
||||
/** 每行最多几个方框 —— 蛇形折行的行宽;取原型 business 预设的观感 */
|
||||
const ROW_WIDTH = 4;
|
||||
|
||||
export function packageToGraph(
|
||||
pkg: ProcessPackage,
|
||||
marks: readonly InstanceMark[] = [],
|
||||
/** 活动范式(activityId → loop|flow):案例活动画绿 —— 范式住活动库,包里只有引用 */
|
||||
paradigms?: ReadonlyMap<string, string>,
|
||||
): FlowGraph {
|
||||
export function packageToGraph(pkg: ProcessPackage, marks: readonly InstanceMark[] = []): FlowGraph {
|
||||
const markOf = new Map(marks.map((m) => [m.nodeId, m]));
|
||||
|
||||
/*
|
||||
@@ -46,38 +41,47 @@ export function packageToGraph(
|
||||
const bottom = stored.nodes.reduce((max, n) => Math.max(max, (n.y ?? 0)), 0) + 90;
|
||||
const extras: FlowNode[] = missing.map((n, i) => ({
|
||||
id: n.id,
|
||||
kind: n.kind === 'gateway' ? ('gateway' as const) : n.kind === 'start' ? ('start' as const) : n.kind === 'end' ? ('end' as const) : ('activity' as const),
|
||||
kind: n.kind === 'start' ? ('start' as const) : n.kind === 'end' ? ('end' as const) : ('activity' as const),
|
||||
label: n.label,
|
||||
...(n.kind === 'gateway' ? { mode: n.gateway === 'parallel' ? ('and' as const) : ('x' as const) } : {}),
|
||||
...(n.kind === 'approval' ? { flavor: 'approval' as const, owner: n.approver.position } : {}),
|
||||
...(n.kind === 'activity' ? { code: n.id, owner: n.owner.position } : {}),
|
||||
...(n.kind === 'activity' ? { code: n.id, owner: n.owner.position, act: n.activityId } : {}),
|
||||
x: 30 + (i % 6) * 160,
|
||||
y: bottom + Math.floor(i / 6) * 60,
|
||||
}));
|
||||
const semanticById = new Map(pkg.nodes.map((n) => [n.id as string, n]));
|
||||
const withState = (list: readonly FlowNode[]) =>
|
||||
list.map((n) => {
|
||||
const state = stateOf(markOf.get(n.id));
|
||||
return state === undefined ? n : { ...n, note: state };
|
||||
const sem = semanticById.get(n.id);
|
||||
// 链接的实现(装载面)以语义为准回填 —— 存证里的 act 只是画布工作痕迹
|
||||
const linked = sem?.kind === 'activity' ? { act: sem.activityId as string } : {};
|
||||
const base = { ...n, ...linked };
|
||||
return state === undefined ? base : { ...base, note: state };
|
||||
});
|
||||
return { ...stored, nodes: [...withState(stored.nodes), ...withState(extras)] };
|
||||
/*
|
||||
* 边属性(条件/映射)从语义边回填到画布边:属性卡编辑的是画布工作副本,
|
||||
* 保存时 graph-to-pkg 又以它为准 —— 回填缺席的话,打开即保存会把契约冲掉。
|
||||
* 经卡点的边按卡点 `to` 认亲(A5→c_reject 即 A5→A4)。
|
||||
*/
|
||||
const calloutTo = new Map(
|
||||
stored.nodes.filter((n) => n.kind === 'callout' && n.to !== undefined).map((n) => [n.id, n.to!]),
|
||||
);
|
||||
const semEdge = new Map(pkg.edges.map((e) => [`${e.from}→${e.to}`, e]));
|
||||
const edges = stored.edges.map((e) => {
|
||||
const sem = semEdge.get(`${e.from}→${calloutTo.get(e.to) ?? e.to}`);
|
||||
if (sem === undefined) return e;
|
||||
return {
|
||||
...e,
|
||||
...(sem.condition === undefined ? {} : { condition: sem.condition }),
|
||||
mapping: sem.mapping as unknown[],
|
||||
};
|
||||
});
|
||||
return { ...stored, edges, nodes: [...withState(stored.nodes), ...withState(extras)] };
|
||||
}
|
||||
|
||||
const nodes: FlowNode[] = pkg.nodes.map((n) => {
|
||||
const state = stateOf(markOf.get(n.id));
|
||||
|
||||
if (n.kind === 'gateway') {
|
||||
return {
|
||||
id: n.id,
|
||||
kind: 'gateway',
|
||||
label: n.label,
|
||||
// 并行网关画 +,异或网关画 ×(判断)
|
||||
mode: n.gateway === 'parallel' ? 'and' : 'x',
|
||||
...(n.sub === undefined ? (n.mode === 'join' ? { sub: '汇合' } : { sub: '分叉' }) : { sub: n.sub }),
|
||||
...(n.basis === undefined ? {} : { basis: n.basis }),
|
||||
...(state === undefined ? {} : { note: state }),
|
||||
};
|
||||
}
|
||||
|
||||
if (n.kind === 'start') return { id: n.id, kind: 'start', label: n.label };
|
||||
if (n.kind === 'end') return { id: n.id, kind: 'end', label: n.label };
|
||||
|
||||
@@ -96,7 +100,7 @@ export function packageToGraph(
|
||||
return {
|
||||
...base,
|
||||
owner: n.owner.position,
|
||||
...(paradigms?.get(n.activityId) === 'loop' ? { flavor: 'case' as const } : {}),
|
||||
act: n.activityId,
|
||||
...(n.in === undefined ? {} : { in: [...n.in] }),
|
||||
...(n.out === undefined ? {} : { out: [...n.out] }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user