@gadmin2n/schematics 0.0.85 → 0.0.87

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.
@@ -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')),
@@ -102,16 +102,20 @@ function RunDot({
102
102
  count,
103
103
  color,
104
104
  label,
105
+ onClick,
105
106
  }: {
106
107
  count: number;
107
108
  color: string;
108
109
  label: string;
110
+ onClick?: () => void;
109
111
  }) {
110
112
  const size = 28;
111
113
  const hasCount = count > 0;
114
+ const clickable = hasCount && !!onClick;
112
115
  return (
113
116
  <Tooltip title={`${label}: ${count}`}>
114
117
  <span
118
+ onClick={clickable ? onClick : undefined}
115
119
  style={{
116
120
  display: 'inline-flex',
117
121
  alignItems: 'center',
@@ -124,7 +128,7 @@ function RunDot({
124
128
  fontSize: 11,
125
129
  fontWeight: 600,
126
130
  color: hasCount ? color : '#d9d9d9',
127
- cursor: 'default',
131
+ cursor: clickable ? 'pointer' : 'default',
128
132
  lineHeight: 1,
129
133
  }}
130
134
  >
@@ -339,14 +343,38 @@ export default function AgendaJobsPage() {
339
343
  title: 'Runs',
340
344
  key: 'runs',
341
345
  width: 170,
342
- render: (_: any, record: JobItem) => (
343
- <Space size={6}>
344
- <RunDot count={record.runs.queued} color="#faad14" label="Queued" />
345
- <RunDot count={record.runs.success} color="#006d32" label="Success" />
346
- <RunDot count={record.runs.running} color="#52c41a" label="Running" />
347
- <RunDot count={record.runs.failed} color="#ff4d4f" label="Failed" />
348
- </Space>
349
- ),
346
+ render: (_: any, record: JobItem) => {
347
+ const go = (status: string) =>
348
+ navigate(`show/${encodeURIComponent(record.name)}?status=${status}`);
349
+ return (
350
+ <Space size={6}>
351
+ <RunDot
352
+ count={record.runs.queued}
353
+ color="#faad14"
354
+ label="Queued"
355
+ onClick={() => go('queued')}
356
+ />
357
+ <RunDot
358
+ count={record.runs.success}
359
+ color="#006d32"
360
+ label="Success"
361
+ onClick={() => go('success')}
362
+ />
363
+ <RunDot
364
+ count={record.runs.running}
365
+ color="#52c41a"
366
+ label="Running"
367
+ onClick={() => go('running')}
368
+ />
369
+ <RunDot
370
+ count={record.runs.failed}
371
+ color="#ff4d4f"
372
+ label="Failed"
373
+ onClick={() => go('failed')}
374
+ />
375
+ </Space>
376
+ );
377
+ },
350
378
  },
351
379
  {
352
380
  title: 'Schedule',
@@ -451,7 +479,7 @@ export default function AgendaJobsPage() {
451
479
  icon={<DatabaseOutlined />}
452
480
  onClick={() => navigate('/admin/data-mngt/agenda-job')}
453
481
  >
454
- AgendaJob 表
482
+ 任务原始表
455
483
  </Button>
456
484
  </Tooltip>
457
485
  <Button
@@ -22,7 +22,7 @@ import {
22
22
  DeleteOutlined,
23
23
  PlayCircleOutlined,
24
24
  } from '@ant-design/icons';
25
- import { useNavigate, useParams } from 'react-router-dom';
25
+ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
26
26
  import { customRequest } from 'helpers/http';
27
27
  import { useECharts } from 'hooks/useECharts';
28
28
  import dayjs from 'dayjs';
@@ -216,6 +216,8 @@ export default function AgendaJobShow() {
216
216
  const { jobName: rawJobName } = useParams<{ jobName: string }>();
217
217
  const jobName = decodeURIComponent(rawJobName || '');
218
218
  const navigate = useNavigate();
219
+ const [searchParams] = useSearchParams();
220
+ const initialStatus = searchParams.get('status') || undefined;
219
221
 
220
222
  const [detail, setDetail] = useState<JobDetail | null>(null);
221
223
  const [loading, setLoading] = useState(true);
@@ -231,7 +233,9 @@ export default function AgendaJobShow() {
231
233
  // Chart filters
232
234
  const [filterBefore, setFilterBefore] = useState<dayjs.Dayjs | null>(dayjs());
233
235
  const [filterLimit, setFilterLimit] = useState(25);
234
- const [filterStatus, setFilterStatus] = useState<string | undefined>();
236
+ const [filterStatus, setFilterStatus] = useState<string | undefined>(
237
+ initialStatus,
238
+ );
235
239
 
236
240
  // Fetch job detail
237
241
  const fetchDetail = useCallback(async () => {
@@ -327,9 +331,10 @@ export default function AgendaJobShow() {
327
331
  if (!detail) {
328
332
  return (
329
333
  <div style={{ padding: 24 }}>
330
- <Button icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)}>
331
- 返回
332
- </Button>
334
+ <Button
335
+ icon={<ArrowLeftOutlined />}
336
+ onClick={() => navigate(-1)}
337
+ ></Button>
333
338
  <div style={{ marginTop: 24, textAlign: 'center', color: '#999' }}>
334
339
  Job 不存在
335
340
  </div>
@@ -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
+ });
@@ -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
@@ -408,9 +440,7 @@ export default function WorkflowEditorPage() {
408
440
  type="text"
409
441
  icon={<ArrowLeftOutlined />}
410
442
  onClick={() => navigate('/admin/workflow')}
411
- >
412
- Back
413
- </Button>
443
+ ></Button>
414
444
  {editingTitle ? (
415
445
  <Input
416
446
  ref={titleInputRef}
@@ -448,7 +478,7 @@ export default function WorkflowEditorPage() {
448
478
  }))}
449
479
  />
450
480
  )}
451
- {isDev && (
481
+ {isDev && viewMode === 'canvas' && (
452
482
  <Button
453
483
  type="primary"
454
484
  icon={<RobotOutlined />}
@@ -470,30 +500,73 @@ export default function WorkflowEditorPage() {
470
500
  </div>
471
501
  </Card>
472
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
+
473
535
  {/* Main area: Flow + Property Panel */}
474
536
  <div
475
537
  style={{ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0 }}
476
538
  >
477
- <div style={{ flex: 1, position: 'relative' }}>
478
- <FlowRenderer
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}
479
565
  dsl={dsl}
480
- nodeTypeMap={nodeTypeMap}
481
- selectedNodeId={selectedNodeId}
482
- onNodeClick={handleNodeClick}
483
- onDslChange={handleDslChange}
566
+ onApply={handleDslApply}
567
+ onDirtyChange={setDslDirty}
484
568
  />
485
- </div>
486
- <NodePropertyPanel
487
- node={selectedNode}
488
- nodeTypes={nodeTypes}
489
- currentWorkflowId={id}
490
- onSave={handleNodePropertySave}
491
- onLabelChange={handleNodeLabelChange}
492
- onAiEdit={handleNodeAiEdit}
493
- onDelete={handleNodeDelete}
494
- onAbort={abortAgent}
495
- aiLoading={agentLoading}
496
- />
569
+ )}
497
570
  </div>
498
571
 
499
572
  {/* Name modal for untitled workflows */}
@@ -420,7 +420,7 @@ export default function WorkflowListPage() {
420
420
  navigate('/admin/data-mngt/workflow-event-outbox')
421
421
  }
422
422
  >
423
- EventOutbox 表
423
+ PG表事件队列
424
424
  </Button>
425
425
  </Tooltip>
426
426
  <Button
@@ -234,9 +234,7 @@ export default function WorkflowInstanceDetailPage() {
234
234
  type="text"
235
235
  icon={<ArrowLeftOutlined />}
236
236
  onClick={() => navigate(-1)}
237
- >
238
- Back
239
- </Button>
237
+ ></Button>
240
238
  <Title level={4} style={{ margin: 0 }}>
241
239
  {data.workflow.name} — Instance #{data.id}
242
240
  </Title>
@@ -197,9 +197,7 @@ export default function WorkflowInstancesPage() {
197
197
  type="text"
198
198
  icon={<ArrowLeftOutlined />}
199
199
  onClick={() => navigate('/admin/workflow')}
200
- >
201
- Back
202
- </Button>
200
+ ></Button>
203
201
  <Title level={4} style={{ margin: 0 }}>
204
202
  {workflowName || 'Workflow'} — Execution History
205
203
  </Title>
@@ -93,9 +93,11 @@ export function NodeInstanceForm({
93
93
  }}
94
94
  >
95
95
  <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
96
- <Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack}>
97
- 返回
98
- </Button>
96
+ <Button
97
+ type="text"
98
+ icon={<ArrowLeftOutlined />}
99
+ onClick={onBack}
100
+ ></Button>
99
101
  <Title level={4} style={{ margin: 0 }}>
100
102
  {title}
101
103
  </Title>
@@ -1,4 +1,10 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from '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
- <div style={{ flex: 1, position: 'relative' }}>
330
- <FlowRenderer
331
- dsl={dsl}
332
- nodeTypeMap={nodeTypeMap}
333
- selectedNodeId={selectedNodeId}
334
- readonly
335
- onNodeClick={(nodeId) => setSelectedNodeId(nodeId || null)}
336
- />
337
- </div>
338
- {selectedNode && (
339
- <div
340
- style={{
341
- width: 320,
342
- flexShrink: 0,
343
- borderLeft: '1px solid #f0f0f0',
344
- background: '#fff',
345
- overflowY: 'auto',
346
- padding: 16,
347
- }}
348
- >
349
- <Title level={5} style={{ marginTop: 0 }}>
350
- {selectedNode.label}
351
- </Title>
352
- <Descriptions column={1} size="small" bordered>
353
- <Descriptions.Item label="ID">
354
- <Typography.Text copyable style={{ fontSize: 12 }}>
355
- {selectedNode.id}
356
- </Typography.Text>
357
- </Descriptions.Item>
358
- <Descriptions.Item label="类型">
359
- {selectedNode.type}
360
- </Descriptions.Item>
361
- <Descriptions.Item label="分类">
362
- <Tag>
363
- {nodeTypeMap[selectedNode.type]?.category || 'ACTION'}
364
- </Tag>
365
- </Descriptions.Item>
366
- {selectedNode.instanceRef && (
367
- <Descriptions.Item label="节点实例">
368
- {selectedNode.instanceRef.instanceName}
369
- </Descriptions.Item>
370
- )}
371
- </Descriptions>
372
- {selectedNode.config &&
373
- Object.keys(selectedNode.config).length > 0 && (
374
- <>
375
- <Title level={5} style={{ marginTop: 16 }}>
376
- 配置
377
- </Title>
378
- <Descriptions column={1} size="small" bordered>
379
- {Object.entries(selectedNode.config).map(([key, value]) => (
380
- <Descriptions.Item key={key} label={key}>
381
- {typeof value === 'object'
382
- ? JSON.stringify(value, null, 2)
383
- : String(value ?? '')}
384
- </Descriptions.Item>
385
- ))}
386
- </Descriptions>
387
- </>
388
- )}
389
- </div>
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [