@gadmin2n/schematics 0.0.95 → 0.0.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/application/files/gadmin2-game-angle-demo/DESIGN.md +348 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +75 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +24 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +32 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.service.ts +28 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +220 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +129 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +46 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +43 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +109 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +19 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +55 -56
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +7 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +7 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +179 -160
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +34 -31
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +24 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +24 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +173 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +27 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +29 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasConfigRegistry.tsx +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasContextMenuRegistry.tsx +98 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/TableConfigModal.tsx +192 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/utils/tableCodeUtils.ts +338 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +66 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +7 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +66 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +7 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +9 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +9 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Table 代码字符串解析 / 改写工具。
|
|
3
|
+
*
|
|
4
|
+
* 这些函数全部以 JSX 字符串为输入输出,不操作 React 组件实例。
|
|
5
|
+
* 由 TableConfigModal 与 canvasContextMenuRegistry 中的 tableActions 共用。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ColumnMeta {
|
|
9
|
+
title: string;
|
|
10
|
+
dataIndex: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SortFilterFlag = true | false | string[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 在 JSX 对象字面量片段中读取一个字符串属性的值,支持双引号 / 单引号字面量
|
|
17
|
+
* 与 \" \\ \n 等转义序列。无法解析时返回 null。
|
|
18
|
+
*/
|
|
19
|
+
function readStringProp(obj: string, propName: string): string | null {
|
|
20
|
+
// 双引号字面量:(?:[^"\\]|\\.)* 表示除 " 和 \ 外的字符 或 反斜杠+任意单字符
|
|
21
|
+
const dq = obj.match(
|
|
22
|
+
new RegExp(`${propName}\\s*:\\s*("(?:[^"\\\\]|\\\\.)*")`),
|
|
23
|
+
);
|
|
24
|
+
if (dq) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(dq[1]);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sq = obj.match(
|
|
32
|
+
new RegExp(`${propName}\\s*:\\s*'((?:[^'\\\\]|\\\\.)*)'`),
|
|
33
|
+
);
|
|
34
|
+
if (sq) {
|
|
35
|
+
// 把单引号字面量转成 JSON 双引号字面量(替换未转义的双引号、保留已转义序列)
|
|
36
|
+
const inner = sq[1].replace(/(^|[^\\])"/g, '$1\\"');
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(`"${inner}"`);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 从 Table 组件的 JSX 字符串中解析 columns 数组里每列的 title/dataIndex。
|
|
48
|
+
*
|
|
49
|
+
* 解析失败(columns 含复杂 render 函数、JSON 不规范等)时返回 null。
|
|
50
|
+
* 调用方据此显示 Alert + 禁用确认。
|
|
51
|
+
*/
|
|
52
|
+
export function parseTableColumnsMeta(code: string): ColumnMeta[] | null {
|
|
53
|
+
const match = code.match(/columns\s*=\s*\{(\[[\s\S]*?\])\}/);
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const arrayLiteral = match[1];
|
|
56
|
+
|
|
57
|
+
// 用 regex 提取每个 { ... } 对象内的 title 和 dataIndex 字符串字面量。
|
|
58
|
+
// 这种方法对含 render: (v) => ... 的列也容错(直接忽略 render)。
|
|
59
|
+
const objects = arrayLiteral.match(/\{[^{}]*\}/g);
|
|
60
|
+
if (!objects || objects.length === 0) return null;
|
|
61
|
+
|
|
62
|
+
const result: ColumnMeta[] = [];
|
|
63
|
+
for (const obj of objects) {
|
|
64
|
+
const title = readStringProp(obj, 'title');
|
|
65
|
+
const dataIndex = readStringProp(obj, 'dataIndex');
|
|
66
|
+
if (title != null && dataIndex != null) {
|
|
67
|
+
result.push({ title, dataIndex });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result.length > 0 ? result : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 从 code 字符串中读取 sortable / filterable prop 当前状态。
|
|
75
|
+
*
|
|
76
|
+
* 返回:
|
|
77
|
+
* - true:未设置或显式 {true}(默认全开)
|
|
78
|
+
* - false:显式 {false}
|
|
79
|
+
* - string[]:显式数组形式 {[...]}
|
|
80
|
+
*/
|
|
81
|
+
export function readTableFlag(
|
|
82
|
+
code: string,
|
|
83
|
+
propName: 'sortable' | 'filterable',
|
|
84
|
+
): SortFilterFlag {
|
|
85
|
+
if (new RegExp(`\\b${propName}\\s*=\\s*\\{false\\}`).test(code)) return false;
|
|
86
|
+
const arrayMatch = code.match(
|
|
87
|
+
new RegExp(`\\b${propName}\\s*=\\s*\\{(\\[[^\\]]*\\])\\}`),
|
|
88
|
+
);
|
|
89
|
+
if (arrayMatch) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(arrayMatch[1].replace(/'/g, '"'));
|
|
92
|
+
if (Array.isArray(parsed)) return parsed as string[];
|
|
93
|
+
} catch {
|
|
94
|
+
// 解析失败回退为 true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 把 sortable / filterable prop 写回 code 字符串。
|
|
102
|
+
*
|
|
103
|
+
* - flag === true:删除 prop(恢复默认)
|
|
104
|
+
* - flag === false:替换/插入 `propName={false}`
|
|
105
|
+
* - flag === string[]:替换/插入 `propName={["a","b"]}`
|
|
106
|
+
*
|
|
107
|
+
* 注入位置:testId= 那行之前;找不到 testId 则回退到 /> 之前。
|
|
108
|
+
*/
|
|
109
|
+
export function writeTableFlag(
|
|
110
|
+
code: string,
|
|
111
|
+
propName: 'sortable' | 'filterable',
|
|
112
|
+
flag: SortFilterFlag,
|
|
113
|
+
): string {
|
|
114
|
+
// 先删除现有 prop(无论什么形式)
|
|
115
|
+
let next = code.replace(
|
|
116
|
+
new RegExp(`\\s*\\b${propName}\\s*=\\s*\\{(true|false|\\[[^\\]]*\\])\\}`),
|
|
117
|
+
'',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (flag === true) return next;
|
|
121
|
+
|
|
122
|
+
const value = flag === false ? '{false}' : `{${JSON.stringify(flag)}}`;
|
|
123
|
+
const inject = `${propName}=${value}`;
|
|
124
|
+
|
|
125
|
+
if (/testId\s*=/.test(next)) {
|
|
126
|
+
next = next.replace(/([ \t]*)(testId\s*=)/, `$1${inject}\n$1$2`);
|
|
127
|
+
} else {
|
|
128
|
+
next = next.replace(/\/>/, ` ${inject}\n/>`);
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── MultiChart-as-table 列配置 helpers ────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/** 在 s 中从 start 位置(指向 open)开始,找出与之匹配的 close 字符的位置;不平衡返回 -1。 */
|
|
136
|
+
function findBalancedClose(
|
|
137
|
+
s: string,
|
|
138
|
+
start: number,
|
|
139
|
+
open: string,
|
|
140
|
+
close: string,
|
|
141
|
+
): number {
|
|
142
|
+
let depth = 0;
|
|
143
|
+
for (let i = start; i < s.length; i++) {
|
|
144
|
+
if (s[i] === open) depth++;
|
|
145
|
+
else if (s[i] === close) {
|
|
146
|
+
depth--;
|
|
147
|
+
if (depth === 0) return i;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 定位 chartConfig={...} 的 span。
|
|
155
|
+
* - start:'chartConfig' 文字开始位置
|
|
156
|
+
* - end:JSX 闭合 '}' 之后的位置(半开区间)
|
|
157
|
+
* - jsonStr:JSX 大括号里面的内容(trim 后),通常是一个 JSON 对象字面量
|
|
158
|
+
*
|
|
159
|
+
* 找不到返回 null。
|
|
160
|
+
*/
|
|
161
|
+
function findChartConfigSpan(
|
|
162
|
+
code: string,
|
|
163
|
+
): { start: number; end: number; jsonStr: string } | null {
|
|
164
|
+
const m = code.match(/chartConfig\s*=\s*\{/);
|
|
165
|
+
if (!m || m.index == null) return null;
|
|
166
|
+
const propStart = m.index;
|
|
167
|
+
const jsxOpen = propStart + m[0].length - 1;
|
|
168
|
+
const jsxClose = findBalancedClose(code, jsxOpen, '{', '}');
|
|
169
|
+
if (jsxClose === -1) return null;
|
|
170
|
+
const jsonStr = code.slice(jsxOpen + 1, jsxClose).trim();
|
|
171
|
+
return { start: propStart, end: jsxClose + 1, jsonStr };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 从 MultiChart code 字符串中解析 data prop,推导出 antd Table columns 元数据。
|
|
176
|
+
* 与 MultiChart 内部 toTableData(data) 保持一致:
|
|
177
|
+
* - 第一列 "分类" / dataIndex "__x"
|
|
178
|
+
* - 后续列对应 data.series 各项的 name
|
|
179
|
+
*
|
|
180
|
+
* 解析失败返回 null(调用方据此显示 Alert + 禁用确认)。
|
|
181
|
+
*
|
|
182
|
+
* 内部用平衡大括号匹配定位 data={{...}},避免 lazy 正则因 series 中嵌套 [...] 而截断。
|
|
183
|
+
*/
|
|
184
|
+
export function parseMultiChartTableColumns(code: string): ColumnMeta[] | null {
|
|
185
|
+
const m = code.match(/data\s*=\s*\{/);
|
|
186
|
+
if (!m || m.index == null) return null;
|
|
187
|
+
const jsxOpen = m.index + m[0].length - 1;
|
|
188
|
+
const jsxClose = findBalancedClose(code, jsxOpen, '{', '}');
|
|
189
|
+
if (jsxClose === -1) return null;
|
|
190
|
+
const expr = code.slice(jsxOpen + 1, jsxClose);
|
|
191
|
+
|
|
192
|
+
// 仅在 series: [...] 范围内抽 name 字段,避免 tooltip.name / itemStyle.name 等污染
|
|
193
|
+
const seriesIdx = expr.search(/series\s*:\s*\[/);
|
|
194
|
+
if (seriesIdx === -1) return null;
|
|
195
|
+
const arrOpen = expr.indexOf('[', seriesIdx);
|
|
196
|
+
if (arrOpen === -1) return null;
|
|
197
|
+
const arrClose = findBalancedClose(expr, arrOpen, '[', ']');
|
|
198
|
+
if (arrClose === -1) return null;
|
|
199
|
+
const seriesBody = expr.slice(arrOpen + 1, arrClose);
|
|
200
|
+
|
|
201
|
+
const nameMatches = Array.from(
|
|
202
|
+
seriesBody.matchAll(/name\s*:\s*['"]([^'"]+)['"]/g),
|
|
203
|
+
);
|
|
204
|
+
const seriesNames = nameMatches.map((mm) => mm[1]);
|
|
205
|
+
if (seriesNames.length === 0) return null;
|
|
206
|
+
|
|
207
|
+
return [
|
|
208
|
+
{ title: '分类', dataIndex: '__x' },
|
|
209
|
+
...seriesNames.map((name) => ({ title: name, dataIndex: name })),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 从 MultiChart code 中读取 chartConfig.table.sortable / .filterable。
|
|
215
|
+
* 三态语义同 readTableFlag。缺省返回 { sortable: true, filterable: true }。
|
|
216
|
+
*/
|
|
217
|
+
export function readMultiChartTableFlags(code: string): {
|
|
218
|
+
sortable: SortFilterFlag;
|
|
219
|
+
filterable: SortFilterFlag;
|
|
220
|
+
} {
|
|
221
|
+
const span = findChartConfigSpan(code);
|
|
222
|
+
if (!span) return { sortable: true, filterable: true };
|
|
223
|
+
|
|
224
|
+
let cfg: Record<string, Record<string, unknown>>;
|
|
225
|
+
try {
|
|
226
|
+
cfg = JSON.parse(span.jsonStr);
|
|
227
|
+
} catch {
|
|
228
|
+
return { sortable: true, filterable: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const tableCfg = cfg.table ?? {};
|
|
232
|
+
const readFlag = (v: unknown): SortFilterFlag => {
|
|
233
|
+
if (v === false) return false;
|
|
234
|
+
if (Array.isArray(v)) return v as string[];
|
|
235
|
+
return true;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
sortable: readFlag(tableCfg.sortable),
|
|
240
|
+
filterable: readFlag(tableCfg.filterable),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 把 sortable / filterable 写回 MultiChart code 的 chartConfig.table。
|
|
246
|
+
* - true → 删除该字段(恢复默认)
|
|
247
|
+
* - false → 写 false
|
|
248
|
+
* - string[] → 写数组
|
|
249
|
+
*
|
|
250
|
+
* 清理空对象:tableCfg 为空 → 删 chartConfig.table;
|
|
251
|
+
* chartConfig 整体为空 → 删 prop(同时吃掉前置的换行与缩进,避免空行)。
|
|
252
|
+
* 保留其它图表的配置(chartConfig.bar / .line 等)不变。
|
|
253
|
+
*/
|
|
254
|
+
export function writeMultiChartTableFlags(
|
|
255
|
+
code: string,
|
|
256
|
+
sortable: SortFilterFlag,
|
|
257
|
+
filterable: SortFilterFlag,
|
|
258
|
+
): string {
|
|
259
|
+
const span = findChartConfigSpan(code);
|
|
260
|
+
let cfg: Record<string, Record<string, unknown>> = {};
|
|
261
|
+
if (span) {
|
|
262
|
+
try {
|
|
263
|
+
cfg = JSON.parse(span.jsonStr);
|
|
264
|
+
} catch {
|
|
265
|
+
cfg = {};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const tableCfg = { ...(cfg.table ?? {}) };
|
|
270
|
+
|
|
271
|
+
if (sortable === true) delete tableCfg.sortable;
|
|
272
|
+
else tableCfg.sortable = sortable;
|
|
273
|
+
|
|
274
|
+
if (filterable === true) delete tableCfg.filterable;
|
|
275
|
+
else tableCfg.filterable = filterable;
|
|
276
|
+
|
|
277
|
+
const newCfg: Record<string, Record<string, unknown>> = { ...cfg };
|
|
278
|
+
if (Object.keys(tableCfg).length === 0) {
|
|
279
|
+
delete newCfg.table;
|
|
280
|
+
} else {
|
|
281
|
+
newCfg.table = tableCfg;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// chartConfig 整体为空 → 删除整段 prop
|
|
285
|
+
if (Object.keys(newCfg).length === 0) {
|
|
286
|
+
if (!span) return code;
|
|
287
|
+
// 向前吃 ' ' / '\t',以及紧贴的一个 '\n',避免留下空行
|
|
288
|
+
let lead = span.start;
|
|
289
|
+
while (lead > 0 && (code[lead - 1] === ' ' || code[lead - 1] === '\t'))
|
|
290
|
+
lead--;
|
|
291
|
+
if (lead > 0 && code[lead - 1] === '\n') lead--;
|
|
292
|
+
return code.slice(0, lead) + code.slice(span.end);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const newCfgStr = `chartConfig={${JSON.stringify(newCfg)}}`;
|
|
296
|
+
if (span) {
|
|
297
|
+
return code.slice(0, span.start) + newCfgStr + code.slice(span.end);
|
|
298
|
+
}
|
|
299
|
+
// 没有 chartConfig prop,注入到 testId 之前
|
|
300
|
+
if (/testId\s*=/.test(code)) {
|
|
301
|
+
return code.replace(/([ \t]*)(testId\s*=)/, `$1${newCfgStr}\n$1$2`);
|
|
302
|
+
}
|
|
303
|
+
return code.replace(/\/>/, ` ${newCfgStr}\n/>`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 构造一段虚拟独立 Table JSX,用于喂给 TableConfigModal。
|
|
308
|
+
* Modal 内部走原有 parseTableColumnsMeta / readTableFlag 路径,
|
|
309
|
+
* 完全感知不到这段 code 是合成的。
|
|
310
|
+
*
|
|
311
|
+
* 注意:columns 必须输出双引号字符串字面量,否则 parseTableColumnsMeta
|
|
312
|
+
* 的正则匹配不到 title / dataIndex。
|
|
313
|
+
*/
|
|
314
|
+
export function buildVirtualTableCode(
|
|
315
|
+
columns: ColumnMeta[],
|
|
316
|
+
flags: { sortable: SortFilterFlag; filterable: SortFilterFlag },
|
|
317
|
+
): string {
|
|
318
|
+
const colsStr = columns
|
|
319
|
+
.map(
|
|
320
|
+
(c) =>
|
|
321
|
+
` { title: ${JSON.stringify(c.title)}, dataIndex: ${JSON.stringify(c.dataIndex)}, key: ${JSON.stringify(c.dataIndex)} }`,
|
|
322
|
+
)
|
|
323
|
+
.join(',\n');
|
|
324
|
+
|
|
325
|
+
const flagToProp = (name: string, flag: SortFilterFlag): string => {
|
|
326
|
+
if (flag === true) return '';
|
|
327
|
+
if (flag === false) return ` ${name}={false}\n`;
|
|
328
|
+
return ` ${name}={${JSON.stringify(flag)}}\n`;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return `<Table
|
|
332
|
+
dataSource={[]}
|
|
333
|
+
columns={[
|
|
334
|
+
${colsStr}
|
|
335
|
+
]}
|
|
336
|
+
${flagToProp('sortable', flags.sortable)}${flagToProp('filterable', flags.filterable)} testId="virtual-multichart-table"
|
|
337
|
+
/>`;
|
|
338
|
+
}
|
|
@@ -13,14 +13,46 @@ const CATEGORY_COLORS: Record<string, { bg: string; border: string }> = {
|
|
|
13
13
|
export interface CustomNodeData {
|
|
14
14
|
label: string;
|
|
15
15
|
category: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
outputs?: string[];
|
|
16
18
|
isSelected: boolean;
|
|
17
19
|
readonly?: boolean;
|
|
18
20
|
[key: string]: unknown;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
const VISIBLE_HANDLE_STYLE: React.CSSProperties = {
|
|
24
|
+
width: 14,
|
|
25
|
+
height: 14,
|
|
26
|
+
background: '#fff',
|
|
27
|
+
border: '2px solid #555',
|
|
28
|
+
cursor: 'crosshair',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const HIDDEN_HANDLE_STYLE: React.CSSProperties = {
|
|
32
|
+
width: 0,
|
|
33
|
+
height: 0,
|
|
34
|
+
border: 'none',
|
|
35
|
+
background: 'transparent',
|
|
36
|
+
visibility: 'hidden',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const BRANCH_LABEL_STYLE: React.CSSProperties = {
|
|
40
|
+
position: 'absolute',
|
|
41
|
+
bottom: -18,
|
|
42
|
+
fontSize: 11,
|
|
43
|
+
color: '#8c8c8c',
|
|
44
|
+
pointerEvents: 'none',
|
|
45
|
+
whiteSpace: 'nowrap',
|
|
46
|
+
};
|
|
47
|
+
|
|
21
48
|
export const CustomNode = memo(({ data }: NodeProps) => {
|
|
22
49
|
const nodeData = data as CustomNodeData;
|
|
23
50
|
const colors = CATEGORY_COLORS[nodeData.category] || CATEGORY_COLORS.ACTION;
|
|
51
|
+
const handleStyle = nodeData.readonly
|
|
52
|
+
? HIDDEN_HANDLE_STYLE
|
|
53
|
+
: VISIBLE_HANDLE_STYLE;
|
|
54
|
+
const outputs = (nodeData.outputs ?? []) as string[];
|
|
55
|
+
const multiOut = outputs.length > 1;
|
|
24
56
|
|
|
25
57
|
return (
|
|
26
58
|
<div
|
|
@@ -37,60 +69,43 @@ export const CustomNode = memo(({ data }: NodeProps) => {
|
|
|
37
69
|
boxShadow: nodeData.isSelected
|
|
38
70
|
? '0 0 0 2px rgba(22,119,255,0.2)'
|
|
39
71
|
: undefined,
|
|
72
|
+
position: 'relative',
|
|
40
73
|
}}
|
|
41
74
|
>
|
|
42
|
-
{
|
|
43
|
-
<Handle
|
|
44
|
-
type="target"
|
|
45
|
-
position={Position.Top}
|
|
46
|
-
style={{
|
|
47
|
-
width: 14,
|
|
48
|
-
height: 14,
|
|
49
|
-
background: '#fff',
|
|
50
|
-
border: '2px solid #555',
|
|
51
|
-
cursor: 'crosshair',
|
|
52
|
-
}}
|
|
53
|
-
/>
|
|
54
|
-
)}
|
|
55
|
-
{nodeData.readonly && (
|
|
56
|
-
<Handle
|
|
57
|
-
type="target"
|
|
58
|
-
position={Position.Top}
|
|
59
|
-
style={{
|
|
60
|
-
width: 0,
|
|
61
|
-
height: 0,
|
|
62
|
-
border: 'none',
|
|
63
|
-
background: 'transparent',
|
|
64
|
-
visibility: 'hidden',
|
|
65
|
-
}}
|
|
66
|
-
/>
|
|
67
|
-
)}
|
|
75
|
+
<Handle type="target" position={Position.Top} style={handleStyle} />
|
|
68
76
|
<div>{nodeData.label}</div>
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
{multiOut ? (
|
|
78
|
+
<>
|
|
79
|
+
{outputs.map((id, idx) => (
|
|
80
|
+
<Handle
|
|
81
|
+
key={id}
|
|
82
|
+
id={id}
|
|
83
|
+
type="source"
|
|
84
|
+
position={Position.Bottom}
|
|
85
|
+
style={{
|
|
86
|
+
...handleStyle,
|
|
87
|
+
left: `${((idx + 1) / (outputs.length + 1)) * 100}%`,
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
{!nodeData.readonly && (
|
|
92
|
+
<>
|
|
93
|
+
{outputs.map((id, idx) => (
|
|
94
|
+
<span
|
|
95
|
+
key={id}
|
|
96
|
+
style={{
|
|
97
|
+
...BRANCH_LABEL_STYLE,
|
|
98
|
+
left: `${((idx + 1) / (outputs.length + 1)) * 100 - 5}%`,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{id}
|
|
102
|
+
</span>
|
|
103
|
+
))}
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</>
|
|
107
|
+
) : (
|
|
108
|
+
<Handle type="source" position={Position.Bottom} style={handleStyle} />
|
|
94
109
|
)}
|
|
95
110
|
</div>
|
|
96
111
|
);
|
|
@@ -18,6 +18,7 @@ import '@xyflow/react/dist/style.css';
|
|
|
18
18
|
import { CustomNode } from './CustomNode';
|
|
19
19
|
import { ExecutionStatusNode } from './ExecutionStatusNode';
|
|
20
20
|
import type { WorkflowDSL, WorkflowNodeType } from '../types';
|
|
21
|
+
import { resolveOutputs } from '../utils/resolveOutputs';
|
|
21
22
|
|
|
22
23
|
interface NodeExecution {
|
|
23
24
|
id: string;
|
|
@@ -36,7 +37,10 @@ const nodeTypes = { custom: CustomNode, executionStatus: ExecutionStatusNode };
|
|
|
36
37
|
|
|
37
38
|
interface EnhancedFlowRendererProps {
|
|
38
39
|
dsl: WorkflowDSL | null;
|
|
39
|
-
nodeTypeMap?: Record<
|
|
40
|
+
nodeTypeMap?: Record<
|
|
41
|
+
string,
|
|
42
|
+
{ category: string; icon: string; outputs?: string[] | null }
|
|
43
|
+
>;
|
|
40
44
|
selectedNodeId?: string | null;
|
|
41
45
|
readonly?: boolean;
|
|
42
46
|
onNodeClick?: (nodeId: string) => void;
|
|
@@ -104,6 +108,8 @@ function EnhancedFlowCanvas({
|
|
|
104
108
|
const nodeData: any = {
|
|
105
109
|
label: n.label,
|
|
106
110
|
category,
|
|
111
|
+
type: n.type,
|
|
112
|
+
outputs: resolveOutputs(n, nodeTypeMap?.[n.type]),
|
|
107
113
|
isSelected: n.id === selectedNodeId,
|
|
108
114
|
readonly: !!readonly,
|
|
109
115
|
};
|
|
@@ -28,6 +28,8 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
28
28
|
export interface ExecutionStatusNodeData {
|
|
29
29
|
label: string;
|
|
30
30
|
category: string;
|
|
31
|
+
type?: string;
|
|
32
|
+
outputs?: string[];
|
|
31
33
|
isSelected: boolean;
|
|
32
34
|
readonly?: boolean;
|
|
33
35
|
executionStatus?:
|
|
@@ -196,32 +198,70 @@ export const ExecutionStatusNode = memo(({ data }: NodeProps) => {
|
|
|
196
198
|
</div>
|
|
197
199
|
)}
|
|
198
200
|
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
201
|
+
{(() => {
|
|
202
|
+
const handleStyle: React.CSSProperties = nodeData.readonly
|
|
203
|
+
? {
|
|
204
|
+
width: 0,
|
|
205
|
+
height: 0,
|
|
206
|
+
border: 'none',
|
|
207
|
+
background: 'transparent',
|
|
208
|
+
visibility: 'hidden',
|
|
209
|
+
}
|
|
210
|
+
: {
|
|
211
|
+
width: 14,
|
|
212
|
+
height: 14,
|
|
213
|
+
background: '#fff',
|
|
214
|
+
border: '2px solid #555',
|
|
215
|
+
cursor: 'crosshair',
|
|
216
|
+
};
|
|
217
|
+
const outputs = (nodeData.outputs ?? []) as string[];
|
|
218
|
+
const multiOut = outputs.length > 1;
|
|
219
|
+
if (multiOut) {
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
{outputs.map((id, idx) => (
|
|
223
|
+
<Handle
|
|
224
|
+
key={id}
|
|
225
|
+
id={id}
|
|
226
|
+
type="source"
|
|
227
|
+
position={Position.Bottom}
|
|
228
|
+
style={{
|
|
229
|
+
...handleStyle,
|
|
230
|
+
left: `${((idx + 1) / (outputs.length + 1)) * 100}%`,
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
))}
|
|
234
|
+
{!nodeData.readonly && (
|
|
235
|
+
<>
|
|
236
|
+
{outputs.map((id, idx) => (
|
|
237
|
+
<span
|
|
238
|
+
key={id}
|
|
239
|
+
style={{
|
|
240
|
+
position: 'absolute',
|
|
241
|
+
bottom: -18,
|
|
242
|
+
left: `${((idx + 1) / (outputs.length + 1)) * 100 - 5}%`,
|
|
243
|
+
fontSize: 11,
|
|
244
|
+
color: '#8c8c8c',
|
|
245
|
+
pointerEvents: 'none',
|
|
246
|
+
whiteSpace: 'nowrap',
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
{id}
|
|
250
|
+
</span>
|
|
251
|
+
))}
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
254
|
+
</>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return (
|
|
258
|
+
<Handle
|
|
259
|
+
type="source"
|
|
260
|
+
position={Position.Bottom}
|
|
261
|
+
style={handleStyle}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
})()}
|
|
225
265
|
</div>
|
|
226
266
|
);
|
|
227
267
|
});
|
|
@@ -17,12 +17,16 @@ import {
|
|
|
17
17
|
import '@xyflow/react/dist/style.css';
|
|
18
18
|
import { CustomNode } from './CustomNode';
|
|
19
19
|
import type { WorkflowDSL, WorkflowNodeType } from '../types';
|
|
20
|
+
import { resolveOutputs } from '../utils/resolveOutputs';
|
|
20
21
|
|
|
21
22
|
const nodeTypes = { custom: CustomNode };
|
|
22
23
|
|
|
23
24
|
interface Props {
|
|
24
25
|
dsl: WorkflowDSL | null;
|
|
25
|
-
nodeTypeMap?: Record<
|
|
26
|
+
nodeTypeMap?: Record<
|
|
27
|
+
string,
|
|
28
|
+
{ category: string; icon: string; outputs?: string[] | null }
|
|
29
|
+
>;
|
|
26
30
|
selectedNodeId?: string | null;
|
|
27
31
|
readonly?: boolean;
|
|
28
32
|
onNodeClick?: (nodeId: string) => void;
|
|
@@ -61,6 +65,8 @@ function FlowCanvas({
|
|
|
61
65
|
data: {
|
|
62
66
|
label: n.label,
|
|
63
67
|
category,
|
|
68
|
+
type: n.type,
|
|
69
|
+
outputs: resolveOutputs(n, nodeTypeMap?.[n.type]),
|
|
64
70
|
isSelected: n.id === selectedNodeId,
|
|
65
71
|
readonly: !!readonly,
|
|
66
72
|
},
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx
CHANGED
|
@@ -79,9 +79,16 @@ export default function WorkflowEditorPage() {
|
|
|
79
79
|
const [aiPromptText, setAiPromptText] = useState('');
|
|
80
80
|
|
|
81
81
|
const nodeTypeMap = useMemo(() => {
|
|
82
|
-
const map: Record<
|
|
82
|
+
const map: Record<
|
|
83
|
+
string,
|
|
84
|
+
{ category: string; icon: string; outputs?: string[] | null }
|
|
85
|
+
> = {};
|
|
83
86
|
for (const nt of nodeTypes) {
|
|
84
|
-
map[nt.type] = {
|
|
87
|
+
map[nt.type] = {
|
|
88
|
+
category: nt.category,
|
|
89
|
+
icon: nt.icon,
|
|
90
|
+
outputs: nt.outputs,
|
|
91
|
+
};
|
|
85
92
|
}
|
|
86
93
|
return map;
|
|
87
94
|
}, [nodeTypes]);
|