@gadmin2n/schematics 0.0.96 → 0.0.98

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.
Files changed (34) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/DESIGN.md +348 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +75 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +24 -1
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +220 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +129 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +1 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +4 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +46 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +27 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +6 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +43 -4
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +109 -0
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +205 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +55 -56
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +7 -3
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +7 -1
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +179 -160
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +34 -31
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +24 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +6 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +6 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +66 -51
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +7 -1
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +66 -26
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +7 -1
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +9 -2
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +30 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +9 -2
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +27 -0
  32. package/package.json +1 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package-lock.json +0 -15579
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package-lock.json +0 -17555
@@ -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
- {!nodeData.readonly && (
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
- {!nodeData.readonly && (
70
- <Handle
71
- type="source"
72
- position={Position.Bottom}
73
- style={{
74
- width: 14,
75
- height: 14,
76
- background: '#fff',
77
- border: '2px solid #555',
78
- cursor: 'crosshair',
79
- }}
80
- />
81
- )}
82
- {nodeData.readonly && (
83
- <Handle
84
- type="source"
85
- position={Position.Bottom}
86
- style={{
87
- width: 0,
88
- height: 0,
89
- border: 'none',
90
- background: 'transparent',
91
- visibility: 'hidden',
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<string, { category: string; icon: string }>;
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
- {!nodeData.readonly && (
200
- <Handle
201
- type="source"
202
- position={Position.Bottom}
203
- style={{
204
- width: 14,
205
- height: 14,
206
- background: '#fff',
207
- border: '2px solid #555',
208
- cursor: 'crosshair',
209
- }}
210
- />
211
- )}
212
- {nodeData.readonly && (
213
- <Handle
214
- type="source"
215
- position={Position.Bottom}
216
- style={{
217
- width: 0,
218
- height: 0,
219
- border: 'none',
220
- background: 'transparent',
221
- visibility: 'hidden',
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<string, { category: string; icon: string }>;
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
  },
@@ -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<string, { category: string; icon: string }> = {};
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] = { category: nt.category, icon: nt.icon };
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]);
@@ -118,6 +118,35 @@ export function useWorkflowAgent({ nodeTypes }: UseWorkflowAgentOptions) {
118
118
  )
119
119
  .join('\n');
120
120
 
121
+ const enumeratedHandleRules = nodeTypes
122
+ .filter((nt) => Array.isArray(nt.outputs) && nt.outputs.length > 0)
123
+ .map(
124
+ (nt) =>
125
+ ` - ${nt.type}: sourceHandle MUST be one of [${nt
126
+ .outputs!.map((o) => `'${o}'`)
127
+ .join(', ')}]`,
128
+ )
129
+ .join('\n');
130
+
131
+ const dynamicHandleRules = nodeTypes
132
+ .filter((nt) => nt.outputs === null)
133
+ .map(
134
+ (nt) =>
135
+ ` - ${nt.type}: sourceHandle MUST match a value listed in this node's config.cases[].value`,
136
+ )
137
+ .join('\n');
138
+
139
+ const noHandleTypes = nodeTypes
140
+ .filter((nt) => Array.isArray(nt.outputs) && nt.outputs.length === 0)
141
+ .map((nt) => nt.type)
142
+ .join(', ');
143
+
144
+ const handleSection = `
145
+ Source handle rules (each edge's sourceHandle MUST match the source node's outputs):
146
+ ${enumeratedHandleRules}
147
+ ${dynamicHandleRules}
148
+ For ALL OTHER node types (${noHandleTypes}), edges MUST NOT include the sourceHandle field — omit it entirely.`;
149
+
121
150
  return `You are a workflow DSL generator. Output ONLY a JSON object wrapped in \`\`\`json code block.
122
151
 
123
152
  Available node types:
@@ -131,6 +160,7 @@ DSL format:
131
160
 
132
161
  Each node: { "id": "uuid", "type": "node_type", "label": "Display Name", "position": {"x": number, "y": number}, "config": {...} }
133
162
  Each edge: { "id": "uuid", "source": "nodeId", "target": "nodeId", "sourceHandle?": "string", "label?": "string" }
163
+ ${handleSection}
134
164
 
135
165
  Position nodes using: x = vertical order (increment by 250 for each sequential step), y = horizontal lane (0 for single-path flows; use different y values spaced by 200 only for parallel branches).`;
136
166
  }, [nodeTypes]);
@@ -81,9 +81,16 @@ export default function WorkflowShowPage() {
81
81
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
82
82
 
83
83
  const nodeTypeMap = useMemo(() => {
84
- const map: Record<string, { category: string; icon: string }> = {};
84
+ const map: Record<
85
+ string,
86
+ { category: string; icon: string; outputs?: string[] | null }
87
+ > = {};
85
88
  for (const nt of nodeTypes) {
86
- map[nt.type] = { category: nt.category, icon: nt.icon };
89
+ map[nt.type] = {
90
+ category: nt.category,
91
+ icon: nt.icon,
92
+ outputs: nt.outputs,
93
+ };
87
94
  }
88
95
  return map;
89
96
  }, [nodeTypes]);
@@ -83,6 +83,7 @@ export interface WorkflowNodeType {
83
83
  configSchema: Record<string, any>;
84
84
  inputSchema: Record<string, any>;
85
85
  outputSchema: Record<string, any>;
86
+ outputs: string[] | null;
86
87
  }
87
88
 
88
89
  export interface NodeInstanceAgentOutput {
@@ -0,0 +1,27 @@
1
+ import type { WorkflowNode } from '../types';
2
+
3
+ /**
4
+ * Resolve the list of named output handles for a node at render time.
5
+ *
6
+ * - `switch` nodes derive their handles from `config.cases[].value` so the
7
+ * canvas reflects user-edited cases without round-tripping through the
8
+ * nodeType meta.
9
+ * - All other node types use the declared `outputs` from the WorkflowNodeType
10
+ * API (Phase 4); a missing or empty list means "render a single anonymous
11
+ * handle", which is what the canvas falls back to.
12
+ */
13
+ export function resolveOutputs(
14
+ node: WorkflowNode,
15
+ nodeTypeMeta: { outputs?: string[] | null } | undefined,
16
+ ): string[] {
17
+ if (node.type === 'switch') {
18
+ const cases = (node.config as any)?.cases as
19
+ | Array<{ value?: string }>
20
+ | undefined;
21
+ if (!Array.isArray(cases)) return [];
22
+ return cases
23
+ .map((c) => c?.value)
24
+ .filter((v): v is string => typeof v === 'string' && v.length > 0);
25
+ }
26
+ return nodeTypeMeta?.outputs ?? [];
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.96",
3
+ "version": "0.0.98",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [