@gadmin2n/schematics 0.0.86 → 0.0.88
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/application.factory.js +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/.gitattributes +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +97 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +110 -65
- package/package.json +1 -1
|
@@ -74,11 +74,6 @@ function generateProject(options, path) {
|
|
|
74
74
|
}
|
|
75
75
|
function generateServer(options, path) {
|
|
76
76
|
const framework = options['server-framework'];
|
|
77
|
-
if (framework === 'iris') {
|
|
78
|
-
return (0, schematics_1.apply)((0, schematics_1.url)((0, core_1.join)('./files/customize/server', framework)), [
|
|
79
|
-
(0, schematics_1.move)((0, core_1.join)(path, 'server')),
|
|
80
|
-
]);
|
|
81
|
-
}
|
|
82
77
|
return (0, schematics_1.apply)((0, schematics_1.url)((0, core_1.join)('./files/customize/server', framework)), [
|
|
83
78
|
(0, schematics_1.template)(Object.assign(Object.assign({}, core_1.strings), options)),
|
|
84
79
|
(0, schematics_1.move)((0, core_1.join)(path, 'server')),
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@faker-js/faker": "^10.4.0",
|
|
91
|
-
"@gadmin2n/prisma-nest-generator": "^0.0.
|
|
91
|
+
"@gadmin2n/prisma-nest-generator": "^0.0.46",
|
|
92
92
|
"@gadmin2n/prisma-react-generator": "^0.0.60",
|
|
93
93
|
"@nestjs/testing": "^10.4.15",
|
|
94
94
|
"@types/cookie-parser": "^1.4.3",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { Alert, Input, message } from 'antd';
|
|
9
|
+
import type { WorkflowDSL } from '../types';
|
|
10
|
+
|
|
11
|
+
const { TextArea } = Input;
|
|
12
|
+
|
|
13
|
+
const EMPTY_TEMPLATE = '{\n "nodes": [],\n "edges": []\n}';
|
|
14
|
+
|
|
15
|
+
interface DslViewProps {
|
|
16
|
+
dsl: WorkflowDSL | null;
|
|
17
|
+
readonly?: boolean;
|
|
18
|
+
onApply?: (newDsl: WorkflowDSL) => void;
|
|
19
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DslViewHandle {
|
|
23
|
+
apply: () => void;
|
|
24
|
+
copy: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stringifyDsl(dsl: WorkflowDSL | null): string {
|
|
28
|
+
if (!dsl) return EMPTY_TEMPLATE;
|
|
29
|
+
return JSON.stringify(dsl, null, 2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 形状级校验:只检查必需字段,深层字段(position/config 等)由画布或保存时暴露 */
|
|
33
|
+
function validateDsl(
|
|
34
|
+
parsed: any,
|
|
35
|
+
): { ok: true; dsl: WorkflowDSL } | { ok: false; error: string } {
|
|
36
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
37
|
+
return { ok: false, error: '根必须是对象 { nodes, edges }' };
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(parsed.nodes)) {
|
|
40
|
+
return { ok: false, error: '字段 nodes 必须是数组' };
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(parsed.edges)) {
|
|
43
|
+
return { ok: false, error: '字段 edges 必须是数组' };
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < parsed.nodes.length; i++) {
|
|
46
|
+
const n = parsed.nodes[i];
|
|
47
|
+
if (!n || typeof n !== 'object') {
|
|
48
|
+
return { ok: false, error: `节点 [${i}] 必须是对象` };
|
|
49
|
+
}
|
|
50
|
+
if (typeof n.id !== 'string' || !n.id) {
|
|
51
|
+
return { ok: false, error: `节点 [${i}] 缺少必需字段:id` };
|
|
52
|
+
}
|
|
53
|
+
if (typeof n.type !== 'string' || !n.type) {
|
|
54
|
+
return { ok: false, error: `节点 [${i}] 缺少必需字段:type` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (let i = 0; i < parsed.edges.length; i++) {
|
|
58
|
+
const e = parsed.edges[i];
|
|
59
|
+
if (!e || typeof e !== 'object') {
|
|
60
|
+
return { ok: false, error: `连线 [${i}] 必须是对象` };
|
|
61
|
+
}
|
|
62
|
+
if (typeof e.id !== 'string' || !e.id) {
|
|
63
|
+
return { ok: false, error: `连线 [${i}] 缺少必需字段:id` };
|
|
64
|
+
}
|
|
65
|
+
if (typeof e.source !== 'string' || !e.source) {
|
|
66
|
+
return { ok: false, error: `连线 [${i}] 缺少必需字段:source` };
|
|
67
|
+
}
|
|
68
|
+
if (typeof e.target !== 'string' || !e.target) {
|
|
69
|
+
return { ok: false, error: `连线 [${i}] 缺少必需字段:target` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, dsl: parsed as WorkflowDSL };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function copyToClipboard(text: string): Promise<boolean> {
|
|
76
|
+
try {
|
|
77
|
+
if (navigator.clipboard?.writeText) {
|
|
78
|
+
await navigator.clipboard.writeText(text);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// fallthrough
|
|
83
|
+
}
|
|
84
|
+
// Fallback for non-secure context
|
|
85
|
+
try {
|
|
86
|
+
const ta = document.createElement('textarea');
|
|
87
|
+
ta.value = text;
|
|
88
|
+
ta.style.position = 'fixed';
|
|
89
|
+
ta.style.opacity = '0';
|
|
90
|
+
document.body.appendChild(ta);
|
|
91
|
+
ta.select();
|
|
92
|
+
const ok = document.execCommand('copy');
|
|
93
|
+
document.body.removeChild(ta);
|
|
94
|
+
return ok;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const DslView = forwardRef<DslViewHandle, DslViewProps>(function DslView(
|
|
101
|
+
{ dsl, readonly = false, onApply, onDirtyChange },
|
|
102
|
+
ref,
|
|
103
|
+
) {
|
|
104
|
+
const initial = useMemo(() => stringifyDsl(dsl), [dsl]);
|
|
105
|
+
const [text, setText] = useState(initial);
|
|
106
|
+
const [error, setError] = useState<string | null>(null);
|
|
107
|
+
|
|
108
|
+
// dsl prop 变化时重置 text(应用、版本切换、AI 生成等场景)
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
setText(initial);
|
|
111
|
+
setError(null);
|
|
112
|
+
onDirtyChange?.(false);
|
|
113
|
+
// onDirtyChange 故意不放进依赖数组:调用方传不稳定函数会导致死循环
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [initial]);
|
|
116
|
+
|
|
117
|
+
function handleTextChange(next: string) {
|
|
118
|
+
setText(next);
|
|
119
|
+
if (error) setError(null);
|
|
120
|
+
if (!readonly) {
|
|
121
|
+
const dirty = next !== initial;
|
|
122
|
+
onDirtyChange?.(dirty);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleApply() {
|
|
127
|
+
let parsed: any;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(text);
|
|
130
|
+
} catch (e: any) {
|
|
131
|
+
setError(`JSON 解析失败:${e?.message ?? String(e)}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const result = validateDsl(parsed);
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
setError(result.error);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const normalized = JSON.stringify(result.dsl, null, 2);
|
|
140
|
+
setText(normalized);
|
|
141
|
+
setError(null);
|
|
142
|
+
onApply?.(result.dsl);
|
|
143
|
+
onDirtyChange?.(false);
|
|
144
|
+
message.success('已应用到画布');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleCopy() {
|
|
148
|
+
const ok = await copyToClipboard(text);
|
|
149
|
+
if (ok) {
|
|
150
|
+
message.success('已复制到剪贴板');
|
|
151
|
+
} else {
|
|
152
|
+
message.error('复制失败,请手动选择文本复制');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
useImperativeHandle(
|
|
157
|
+
ref,
|
|
158
|
+
() => ({
|
|
159
|
+
apply: handleApply,
|
|
160
|
+
copy: handleCopy,
|
|
161
|
+
}),
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
[text, onApply, onDirtyChange],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
style={{
|
|
169
|
+
flex: 1,
|
|
170
|
+
display: 'flex',
|
|
171
|
+
flexDirection: 'column',
|
|
172
|
+
minHeight: 0,
|
|
173
|
+
background: '#fff',
|
|
174
|
+
padding: 16,
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{error && (
|
|
178
|
+
<Alert
|
|
179
|
+
type="error"
|
|
180
|
+
message={error}
|
|
181
|
+
showIcon
|
|
182
|
+
closable
|
|
183
|
+
onClose={() => setError(null)}
|
|
184
|
+
style={{ marginBottom: 8, flexShrink: 0 }}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
<TextArea
|
|
189
|
+
value={text}
|
|
190
|
+
onChange={(e) => handleTextChange(e.target.value)}
|
|
191
|
+
readOnly={readonly}
|
|
192
|
+
spellCheck={false}
|
|
193
|
+
autoSize={false}
|
|
194
|
+
style={{
|
|
195
|
+
flex: 1,
|
|
196
|
+
fontFamily:
|
|
197
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
198
|
+
fontSize: 13,
|
|
199
|
+
lineHeight: 1.5,
|
|
200
|
+
resize: 'none',
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
});
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Input,
|
|
12
12
|
message,
|
|
13
13
|
Modal,
|
|
14
|
+
Segmented,
|
|
14
15
|
Select,
|
|
15
16
|
Space,
|
|
16
17
|
Spin,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from 'antd';
|
|
20
21
|
import {
|
|
21
22
|
ArrowLeftOutlined,
|
|
23
|
+
CheckOutlined,
|
|
22
24
|
EditOutlined,
|
|
23
25
|
RobotOutlined,
|
|
24
26
|
RocketOutlined,
|
|
@@ -28,6 +30,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
28
30
|
import { customRequest } from 'helpers/http';
|
|
29
31
|
import { FlowRenderer } from './components/FlowRenderer';
|
|
30
32
|
import { NodePropertyPanel } from './components/NodePropertyPanel';
|
|
33
|
+
import { DslView, type DslViewHandle } from './components/DslView';
|
|
31
34
|
import { useWorkflowAgent } from './hooks/useWorkflowAgent';
|
|
32
35
|
import type {
|
|
33
36
|
Workflow,
|
|
@@ -59,6 +62,9 @@ export default function WorkflowEditorPage() {
|
|
|
59
62
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
60
63
|
|
|
61
64
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
65
|
+
const [viewMode, setViewMode] = useState<'canvas' | 'dsl'>('canvas');
|
|
66
|
+
const [dslDirty, setDslDirty] = useState(false);
|
|
67
|
+
const dslViewRef = useRef<DslViewHandle>(null);
|
|
62
68
|
|
|
63
69
|
// Title editing state
|
|
64
70
|
const [title, setTitle] = useState(isNewMode ? 'Untitled Workflow' : '');
|
|
@@ -334,10 +340,12 @@ export default function WorkflowEditorPage() {
|
|
|
334
340
|
async function handleSelectVersion(version: number) {
|
|
335
341
|
if (!id) return;
|
|
336
342
|
|
|
337
|
-
if (hasUnsavedChanges) {
|
|
343
|
+
if (hasUnsavedChanges || (viewMode === 'dsl' && dslDirty)) {
|
|
338
344
|
Modal.confirm({
|
|
339
|
-
title: '未保存的修改',
|
|
340
|
-
content:
|
|
345
|
+
title: hasUnsavedChanges ? '未保存的修改' : 'DSL 有未应用的编辑',
|
|
346
|
+
content: hasUnsavedChanges
|
|
347
|
+
? '当前有未保存的修改,切换版本将丢失这些更改。确定要继续吗?'
|
|
348
|
+
: 'DSL 视图中有未应用到画布的编辑,切换版本将丢弃这些内容。确定要继续吗?',
|
|
341
349
|
okText: '确定切换',
|
|
342
350
|
cancelText: '取消',
|
|
343
351
|
onOk: () => doLoadVersion(version),
|
|
@@ -369,6 +377,30 @@ export default function WorkflowEditorPage() {
|
|
|
369
377
|
setSelectedNodeId(nodeId || null);
|
|
370
378
|
}
|
|
371
379
|
|
|
380
|
+
function handleViewModeChange(next: 'canvas' | 'dsl') {
|
|
381
|
+
if (next === 'canvas' && dslDirty) {
|
|
382
|
+
Modal.confirm({
|
|
383
|
+
title: '未应用的修改',
|
|
384
|
+
content:
|
|
385
|
+
'DSL 视图中有未应用到画布的修改,切换到 Canvas 将丢弃这些修改。确定要继续吗?',
|
|
386
|
+
okText: '确定切换',
|
|
387
|
+
cancelText: '取消',
|
|
388
|
+
onOk: () => {
|
|
389
|
+
setDslDirty(false);
|
|
390
|
+
setViewMode('canvas');
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
setViewMode(next);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function handleDslApply(newDsl: WorkflowDSL) {
|
|
399
|
+
setDsl(newDsl);
|
|
400
|
+
setHasUnsavedChanges(true);
|
|
401
|
+
setSelectedNodeId(null);
|
|
402
|
+
}
|
|
403
|
+
|
|
372
404
|
if (loading) {
|
|
373
405
|
return (
|
|
374
406
|
<div
|
|
@@ -446,7 +478,7 @@ export default function WorkflowEditorPage() {
|
|
|
446
478
|
}))}
|
|
447
479
|
/>
|
|
448
480
|
)}
|
|
449
|
-
{isDev && (
|
|
481
|
+
{isDev && viewMode === 'canvas' && (
|
|
450
482
|
<Button
|
|
451
483
|
type="primary"
|
|
452
484
|
icon={<RobotOutlined />}
|
|
@@ -468,30 +500,73 @@ export default function WorkflowEditorPage() {
|
|
|
468
500
|
</div>
|
|
469
501
|
</Card>
|
|
470
502
|
|
|
503
|
+
{/* View toggle row + DSL actions */}
|
|
504
|
+
<div
|
|
505
|
+
style={{
|
|
506
|
+
marginBottom: 12,
|
|
507
|
+
flexShrink: 0,
|
|
508
|
+
display: 'flex',
|
|
509
|
+
alignItems: 'center',
|
|
510
|
+
justifyContent: 'space-between',
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
513
|
+
<Segmented
|
|
514
|
+
value={viewMode}
|
|
515
|
+
onChange={(v) => handleViewModeChange(v as 'canvas' | 'dsl')}
|
|
516
|
+
options={[
|
|
517
|
+
{ label: 'Canvas', value: 'canvas' },
|
|
518
|
+
{ label: 'DSL', value: 'dsl' },
|
|
519
|
+
]}
|
|
520
|
+
/>
|
|
521
|
+
{viewMode === 'dsl' && (
|
|
522
|
+
<Space>
|
|
523
|
+
{dslDirty && <Tag color="orange">未应用</Tag>}
|
|
524
|
+
<Button
|
|
525
|
+
type="primary"
|
|
526
|
+
icon={<CheckOutlined />}
|
|
527
|
+
onClick={() => dslViewRef.current?.apply()}
|
|
528
|
+
>
|
|
529
|
+
应用到画布
|
|
530
|
+
</Button>
|
|
531
|
+
</Space>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
|
|
471
535
|
{/* Main area: Flow + Property Panel */}
|
|
472
536
|
<div
|
|
473
537
|
style={{ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0 }}
|
|
474
538
|
>
|
|
475
|
-
|
|
476
|
-
|
|
539
|
+
{viewMode === 'canvas' ? (
|
|
540
|
+
<>
|
|
541
|
+
<div style={{ flex: 1, position: 'relative' }}>
|
|
542
|
+
<FlowRenderer
|
|
543
|
+
dsl={dsl}
|
|
544
|
+
nodeTypeMap={nodeTypeMap}
|
|
545
|
+
selectedNodeId={selectedNodeId}
|
|
546
|
+
onNodeClick={handleNodeClick}
|
|
547
|
+
onDslChange={handleDslChange}
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
<NodePropertyPanel
|
|
551
|
+
node={selectedNode}
|
|
552
|
+
nodeTypes={nodeTypes}
|
|
553
|
+
currentWorkflowId={id}
|
|
554
|
+
onSave={handleNodePropertySave}
|
|
555
|
+
onLabelChange={handleNodeLabelChange}
|
|
556
|
+
onAiEdit={handleNodeAiEdit}
|
|
557
|
+
onDelete={handleNodeDelete}
|
|
558
|
+
onAbort={abortAgent}
|
|
559
|
+
aiLoading={agentLoading}
|
|
560
|
+
/>
|
|
561
|
+
</>
|
|
562
|
+
) : (
|
|
563
|
+
<DslView
|
|
564
|
+
ref={dslViewRef}
|
|
477
565
|
dsl={dsl}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
onNodeClick={handleNodeClick}
|
|
481
|
-
onDslChange={handleDslChange}
|
|
566
|
+
onApply={handleDslApply}
|
|
567
|
+
onDirtyChange={setDslDirty}
|
|
482
568
|
/>
|
|
483
|
-
|
|
484
|
-
<NodePropertyPanel
|
|
485
|
-
node={selectedNode}
|
|
486
|
-
nodeTypes={nodeTypes}
|
|
487
|
-
currentWorkflowId={id}
|
|
488
|
-
onSave={handleNodePropertySave}
|
|
489
|
-
onLabelChange={handleNodeLabelChange}
|
|
490
|
-
onAiEdit={handleNodeAiEdit}
|
|
491
|
-
onDelete={handleNodeDelete}
|
|
492
|
-
onAbort={abortAgent}
|
|
493
|
-
aiLoading={agentLoading}
|
|
494
|
-
/>
|
|
569
|
+
)}
|
|
495
570
|
</div>
|
|
496
571
|
|
|
497
572
|
{/* Name modal for untitled workflows */}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import {
|
|
3
9
|
Button,
|
|
4
10
|
Card,
|
|
@@ -6,6 +12,7 @@ import {
|
|
|
6
12
|
Input,
|
|
7
13
|
message,
|
|
8
14
|
Modal,
|
|
15
|
+
Segmented,
|
|
9
16
|
Select,
|
|
10
17
|
Space,
|
|
11
18
|
Spin,
|
|
@@ -14,6 +21,7 @@ import {
|
|
|
14
21
|
} from 'antd';
|
|
15
22
|
import {
|
|
16
23
|
ArrowLeftOutlined,
|
|
24
|
+
CopyOutlined,
|
|
17
25
|
DiffOutlined,
|
|
18
26
|
EditOutlined,
|
|
19
27
|
HistoryOutlined,
|
|
@@ -24,6 +32,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
24
32
|
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
|
|
25
33
|
import { customRequest } from 'helpers/http';
|
|
26
34
|
import { FlowRenderer } from './components/FlowRenderer';
|
|
35
|
+
import { DslView, type DslViewHandle } from './components/DslView';
|
|
27
36
|
import { RunWorkflowModal } from './components/RunWorkflowModal';
|
|
28
37
|
import type {
|
|
29
38
|
Workflow,
|
|
@@ -52,6 +61,8 @@ export default function WorkflowShowPage() {
|
|
|
52
61
|
|
|
53
62
|
const [dsl, setDsl] = useState<WorkflowDSL | null>(null);
|
|
54
63
|
const [currentVersion, setCurrentVersion] = useState<number | null>(null);
|
|
64
|
+
const [viewMode, setViewMode] = useState<'canvas' | 'dsl'>('canvas');
|
|
65
|
+
const dslViewRef = useRef<DslViewHandle>(null);
|
|
55
66
|
|
|
56
67
|
// Publish modal state
|
|
57
68
|
const [publishModalOpen, setPublishModalOpen] = useState(false);
|
|
@@ -260,9 +271,7 @@ ${diffRightDsl}
|
|
|
260
271
|
type="text"
|
|
261
272
|
icon={<ArrowLeftOutlined />}
|
|
262
273
|
onClick={() => navigate('/admin/workflow')}
|
|
263
|
-
>
|
|
264
|
-
Back
|
|
265
|
-
</Button>
|
|
274
|
+
></Button>
|
|
266
275
|
<Title level={4} style={{ margin: 0 }}>
|
|
267
276
|
{workflow?.name || 'Workflow'}
|
|
268
277
|
</Title>
|
|
@@ -324,69 +333,105 @@ ${diffRightDsl}
|
|
|
324
333
|
</div>
|
|
325
334
|
</Card>
|
|
326
335
|
|
|
336
|
+
{/* View toggle row + DSL actions */}
|
|
337
|
+
<div
|
|
338
|
+
style={{
|
|
339
|
+
marginBottom: 12,
|
|
340
|
+
flexShrink: 0,
|
|
341
|
+
display: 'flex',
|
|
342
|
+
alignItems: 'center',
|
|
343
|
+
justifyContent: 'space-between',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<Segmented
|
|
347
|
+
value={viewMode}
|
|
348
|
+
onChange={(v) => setViewMode(v as 'canvas' | 'dsl')}
|
|
349
|
+
options={[
|
|
350
|
+
{ label: 'Canvas', value: 'canvas' },
|
|
351
|
+
{ label: 'DSL', value: 'dsl' },
|
|
352
|
+
]}
|
|
353
|
+
/>
|
|
354
|
+
{viewMode === 'dsl' && (
|
|
355
|
+
<Button
|
|
356
|
+
icon={<CopyOutlined />}
|
|
357
|
+
onClick={() => dslViewRef.current?.copy()}
|
|
358
|
+
>
|
|
359
|
+
Copy
|
|
360
|
+
</Button>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
|
|
327
364
|
{/* Canvas + Node Detail Panel */}
|
|
328
365
|
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{selectedNode.instanceRef
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<Descriptions
|
|
381
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
366
|
+
{viewMode === 'canvas' ? (
|
|
367
|
+
<>
|
|
368
|
+
<div style={{ flex: 1, position: 'relative' }}>
|
|
369
|
+
<FlowRenderer
|
|
370
|
+
dsl={dsl}
|
|
371
|
+
nodeTypeMap={nodeTypeMap}
|
|
372
|
+
selectedNodeId={selectedNodeId}
|
|
373
|
+
readonly
|
|
374
|
+
onNodeClick={(nodeId) => setSelectedNodeId(nodeId || null)}
|
|
375
|
+
/>
|
|
376
|
+
</div>
|
|
377
|
+
{selectedNode && (
|
|
378
|
+
<div
|
|
379
|
+
style={{
|
|
380
|
+
width: 320,
|
|
381
|
+
flexShrink: 0,
|
|
382
|
+
borderLeft: '1px solid #f0f0f0',
|
|
383
|
+
background: '#fff',
|
|
384
|
+
overflowY: 'auto',
|
|
385
|
+
padding: 16,
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<Title level={5} style={{ marginTop: 0 }}>
|
|
389
|
+
{selectedNode.label}
|
|
390
|
+
</Title>
|
|
391
|
+
<Descriptions column={1} size="small" bordered>
|
|
392
|
+
<Descriptions.Item label="ID">
|
|
393
|
+
<Typography.Text copyable style={{ fontSize: 12 }}>
|
|
394
|
+
{selectedNode.id}
|
|
395
|
+
</Typography.Text>
|
|
396
|
+
</Descriptions.Item>
|
|
397
|
+
<Descriptions.Item label="类型">
|
|
398
|
+
{selectedNode.type}
|
|
399
|
+
</Descriptions.Item>
|
|
400
|
+
<Descriptions.Item label="分类">
|
|
401
|
+
<Tag>
|
|
402
|
+
{nodeTypeMap[selectedNode.type]?.category || 'ACTION'}
|
|
403
|
+
</Tag>
|
|
404
|
+
</Descriptions.Item>
|
|
405
|
+
{selectedNode.instanceRef && (
|
|
406
|
+
<Descriptions.Item label="节点实例">
|
|
407
|
+
{selectedNode.instanceRef.instanceName}
|
|
408
|
+
</Descriptions.Item>
|
|
409
|
+
)}
|
|
410
|
+
</Descriptions>
|
|
411
|
+
{selectedNode.config &&
|
|
412
|
+
Object.keys(selectedNode.config).length > 0 && (
|
|
413
|
+
<>
|
|
414
|
+
<Title level={5} style={{ marginTop: 16 }}>
|
|
415
|
+
配置
|
|
416
|
+
</Title>
|
|
417
|
+
<Descriptions column={1} size="small" bordered>
|
|
418
|
+
{Object.entries(selectedNode.config).map(
|
|
419
|
+
([key, value]) => (
|
|
420
|
+
<Descriptions.Item key={key} label={key}>
|
|
421
|
+
{typeof value === 'object'
|
|
422
|
+
? JSON.stringify(value, null, 2)
|
|
423
|
+
: String(value ?? '')}
|
|
424
|
+
</Descriptions.Item>
|
|
425
|
+
),
|
|
426
|
+
)}
|
|
427
|
+
</Descriptions>
|
|
428
|
+
</>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</>
|
|
433
|
+
) : (
|
|
434
|
+
<DslView ref={dslViewRef} dsl={dsl} readonly />
|
|
390
435
|
)}
|
|
391
436
|
</div>
|
|
392
437
|
|