@redvars/peacock 3.5.1 → 3.6.1
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/{BaseButton-DuASuVth.js → BaseButton-BNFAYn-S.js} +2 -2
- package/dist/{BaseButton-DuASuVth.js.map → BaseButton-BNFAYn-S.js.map} +1 -1
- package/dist/BaseInput-14YmcfK7.js +27 -0
- package/dist/BaseInput-14YmcfK7.js.map +1 -0
- package/dist/banner.js +2 -3
- package/dist/banner.js.map +1 -1
- package/dist/{button-DouvOfEU.js → button-colors-Ccys3hvS.js} +5 -294
- package/dist/button-colors-Ccys3hvS.js.map +1 -0
- package/dist/button-group.js +226 -6
- package/dist/button-group.js.map +1 -1
- package/dist/button.js +294 -8
- package/dist/button.js.map +1 -1
- package/dist/calendar-column-view.js +634 -0
- package/dist/calendar-column-view.js.map +1 -0
- package/dist/calendar-event-BrQ_SEKD.js +199 -0
- package/dist/calendar-event-BrQ_SEKD.js.map +1 -0
- package/dist/calendar-month-view.js +376 -0
- package/dist/calendar-month-view.js.map +1 -0
- package/dist/calendar.js +339 -0
- package/dist/calendar.js.map +1 -0
- package/dist/canvas.js +361 -0
- package/dist/canvas.js.map +1 -0
- package/dist/cb-compound-expression.js +125 -0
- package/dist/cb-compound-expression.js.map +1 -0
- package/dist/cb-divider.js +150 -0
- package/dist/cb-divider.js.map +1 -0
- package/dist/cb-expression.js +75 -0
- package/dist/cb-expression.js.map +1 -0
- package/dist/cb-predicate.js +137 -0
- package/dist/cb-predicate.js.map +1 -0
- package/dist/code-editor.js +2 -1
- package/dist/code-editor.js.map +1 -1
- package/dist/code-highlighter.js +1 -1
- package/dist/code-highlighter.js.map +1 -1
- package/dist/condition-builder.js +58 -0
- package/dist/condition-builder.js.map +1 -0
- package/dist/custom-elements-jsdocs.json +8479 -3965
- package/dist/custom-elements.json +15228 -7544
- package/dist/dropdown-button.js +216 -0
- package/dist/dropdown-button.js.map +1 -0
- package/dist/event-manager-D-QCmUgR.js +113 -0
- package/dist/event-manager-D-QCmUgR.js.map +1 -0
- package/dist/fab.js +1 -1
- package/dist/flow-designer-DvTUrDp5.js +1656 -0
- package/dist/flow-designer-DvTUrDp5.js.map +1 -0
- package/dist/flow-designer-node-BWrPuxAR.js +548 -0
- package/dist/flow-designer-node-BWrPuxAR.js.map +1 -0
- package/dist/flow-designer-node.js +4 -0
- package/dist/flow-designer-node.js.map +1 -0
- package/dist/flow-designer.js +16 -0
- package/dist/flow-designer.js.map +1 -0
- package/dist/html-editor.js +27516 -0
- package/dist/html-editor.js.map +1 -0
- package/dist/icon-button-CK1ZuE-2.js +247 -0
- package/dist/icon-button-CK1ZuE-2.js.map +1 -0
- package/dist/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/{is-dark-mode-DicqGkCJ.js → is-dark-mode-DOcaw4Yq.js} +2 -27
- package/dist/is-dark-mode-DOcaw4Yq.js.map +1 -0
- package/dist/modal.js +412 -0
- package/dist/modal.js.map +1 -0
- package/dist/{navigation-rail-Lxetd5-Z.js → navigation-rail-DTTkqohi.js} +1049 -2391
- package/dist/navigation-rail-DTTkqohi.js.map +1 -0
- package/dist/notification-manager.js +268 -0
- package/dist/notification-manager.js.map +1 -0
- package/dist/peacock-loader.js +93 -8
- package/dist/peacock-loader.js.map +1 -1
- package/dist/popover-NC7b1lTq.js +1971 -0
- package/dist/popover-NC7b1lTq.js.map +1 -0
- package/dist/popover-content.js +125 -0
- package/dist/popover-content.js.map +1 -0
- package/dist/popover.js +4 -0
- package/dist/popover.js.map +1 -0
- package/dist/split-button.js +388 -0
- package/dist/split-button.js.map +1 -0
- package/dist/src/__controllers/floating-controller.d.ts +35 -0
- package/dist/src/calendar/base-event.d.ts +10 -0
- package/dist/src/calendar/calendar-column-view.d.ts +41 -0
- package/dist/src/calendar/calendar-event.d.ts +7 -0
- package/dist/src/calendar/calendar-month-view.d.ts +31 -0
- package/dist/src/calendar/calendar.d.ts +65 -0
- package/dist/src/calendar/event-manager.d.ts +17 -0
- package/dist/src/calendar/index.d.ts +4 -0
- package/dist/src/calendar/types.d.ts +13 -0
- package/dist/src/calendar/utils.d.ts +31 -0
- package/dist/src/canvas/canvas.d.ts +92 -0
- package/dist/src/canvas/index.d.ts +2 -0
- package/dist/src/condition-builder/cb-compound-expression.d.ts +31 -0
- package/dist/src/condition-builder/cb-divider.d.ts +26 -0
- package/dist/src/condition-builder/cb-expression.d.ts +31 -0
- package/dist/src/condition-builder/cb-predicate.d.ts +30 -0
- package/dist/src/condition-builder/condition-builder.d.ts +27 -0
- package/dist/src/condition-builder/index.d.ts +5 -0
- package/dist/src/dropdown-button/dropdown-button.d.ts +68 -0
- package/dist/src/dropdown-button/index.d.ts +1 -0
- package/dist/src/flow-designer/commands.d.ts +66 -0
- package/dist/src/flow-designer/flow-designer-node.d.ts +46 -0
- package/dist/src/flow-designer/flow-designer.d.ts +133 -0
- package/dist/src/flow-designer/index.d.ts +7 -0
- package/dist/src/flow-designer/layout.d.ts +30 -0
- package/dist/src/flow-designer/types.d.ts +142 -0
- package/dist/src/flow-designer/validation.d.ts +43 -0
- package/dist/src/flow-designer/workflow-utils.d.ts +40 -0
- package/dist/src/html-editor/html-editor.d.ts +89 -0
- package/dist/src/html-editor/index.d.ts +2 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/list/index.d.ts +2 -0
- package/dist/src/list/list-item.d.ts +35 -0
- package/dist/src/list/list.d.ts +28 -0
- package/dist/src/menu/menu/menu.d.ts +5 -7
- package/dist/src/menu/menu-item/menu-item.d.ts +14 -13
- package/dist/src/modal/index.d.ts +1 -0
- package/dist/src/modal/modal.d.ts +57 -0
- package/dist/src/navigation-rail/navigation-rail.d.ts +3 -7
- package/dist/src/notification-manager/index.d.ts +1 -0
- package/dist/src/notification-manager/notification-manager.d.ts +44 -0
- package/dist/src/number-field/number-field.d.ts +2 -2
- package/dist/src/popover/index.d.ts +2 -0
- package/dist/src/popover/popover-content.d.ts +29 -0
- package/dist/src/popover/popover.d.ts +62 -0
- package/dist/src/split-button/index.d.ts +1 -0
- package/dist/src/split-button/split-button.d.ts +72 -0
- package/dist/src/svg/index.d.ts +1 -0
- package/dist/src/svg/svg.d.ts +38 -0
- package/dist/src/toolbar/toolbar.d.ts +3 -3
- package/dist/src/tooltip/tooltip.d.ts +2 -15
- package/dist/test/flow-designer.test.d.ts +1 -0
- package/dist/toolbar.js +3 -3
- package/dist/toolbar.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -2
- package/readme.md +3 -3
- package/src/__controllers/floating-controller.ts +237 -0
- package/src/banner/banner.scss +2 -3
- package/src/button/button/button.ts +1 -0
- package/src/calendar/base-event.ts +49 -0
- package/src/calendar/calendar-column-view.scss +326 -0
- package/src/calendar/calendar-column-view.ts +392 -0
- package/src/calendar/calendar-event.ts +20 -0
- package/src/calendar/calendar-month-view.scss +192 -0
- package/src/calendar/calendar-month-view.ts +244 -0
- package/src/calendar/calendar.scss +71 -0
- package/src/calendar/calendar.ts +298 -0
- package/src/calendar/event-manager.ts +117 -0
- package/src/calendar/index.ts +4 -0
- package/src/calendar/types.ts +14 -0
- package/src/calendar/utils.ts +180 -0
- package/src/canvas/canvas.scss +60 -0
- package/src/canvas/canvas.ts +391 -0
- package/src/canvas/index.ts +2 -0
- package/src/code-highlighter/code-highlighter.ts +1 -1
- package/src/condition-builder/cb-compound-expression.scss +37 -0
- package/src/condition-builder/cb-compound-expression.ts +80 -0
- package/src/condition-builder/cb-divider.scss +93 -0
- package/src/condition-builder/cb-divider.ts +56 -0
- package/src/condition-builder/cb-expression.scss +14 -0
- package/src/condition-builder/cb-expression.ts +49 -0
- package/src/condition-builder/cb-predicate.scss +35 -0
- package/src/condition-builder/cb-predicate.ts +102 -0
- package/src/condition-builder/condition-builder.scss +13 -0
- package/src/condition-builder/condition-builder.ts +38 -0
- package/src/condition-builder/index.ts +5 -0
- package/src/dropdown-button/demo/index.html +110 -0
- package/src/dropdown-button/dropdown-button.scss +22 -0
- package/src/dropdown-button/dropdown-button.ts +206 -0
- package/src/dropdown-button/index.ts +1 -0
- package/src/flow-designer/DEMO.md +239 -0
- package/src/flow-designer/commands.ts +278 -0
- package/src/flow-designer/flow-designer-node.ts +172 -0
- package/src/flow-designer/flow-designer.scss +457 -0
- package/src/flow-designer/flow-designer.ts +611 -0
- package/src/flow-designer/index.ts +41 -0
- package/src/flow-designer/layout.ts +357 -0
- package/src/flow-designer/types.ts +166 -0
- package/src/flow-designer/validation.ts +284 -0
- package/src/flow-designer/workflow-utils.ts +282 -0
- package/src/html-editor/html-editor.scss +188 -0
- package/src/html-editor/html-editor.ts +491 -0
- package/src/html-editor/index.ts +3 -0
- package/src/index.ts +27 -1
- package/src/list/index.ts +2 -0
- package/src/list/list-item.scss +111 -0
- package/src/list/list-item.ts +175 -0
- package/src/list/list.scss +24 -0
- package/src/list/list.ts +51 -0
- package/src/menu/menu/menu.scss +2 -2
- package/src/menu/menu/menu.ts +91 -101
- package/src/menu/menu-item/menu-item.scss +4 -0
- package/src/menu/menu-item/menu-item.ts +82 -78
- package/src/modal/index.ts +1 -0
- package/src/modal/modal.scss +206 -0
- package/src/modal/modal.ts +195 -0
- package/src/navigation-rail/navigation-rail-item.scss +7 -38
- package/src/navigation-rail/navigation-rail-item.ts +1 -2
- package/src/navigation-rail/navigation-rail.scss +17 -21
- package/src/navigation-rail/navigation-rail.ts +6 -9
- package/src/notification-manager/index.ts +1 -0
- package/src/notification-manager/notification-manager.scss +113 -0
- package/src/notification-manager/notification-manager.ts +199 -0
- package/src/number-field/number-field.ts +2 -2
- package/src/peacock-loader.ts +83 -0
- package/src/popover/index.ts +2 -0
- package/src/popover/popover-content.scss +69 -0
- package/src/popover/popover-content.ts +51 -0
- package/src/popover/popover.scss +7 -0
- package/src/popover/popover.ts +170 -0
- package/src/split-button/index.ts +1 -0
- package/src/split-button/split-button-colors.scss +56 -0
- package/src/split-button/split-button-sizes.scss +28 -0
- package/src/split-button/split-button.scss +79 -0
- package/src/split-button/split-button.ts +236 -0
- package/src/svg/index.ts +1 -0
- package/src/svg/svg.scss +91 -0
- package/src/svg/svg.ts +160 -0
- package/src/table/table.ts +2 -2
- package/src/toolbar/toolbar.ts +3 -3
- package/src/tooltip/tooltip.scss +4 -3
- package/src/tooltip/tooltip.ts +46 -104
- package/dist/button-DouvOfEU.js.map +0 -1
- package/dist/button-group-CEdMwvJJ.js +0 -464
- package/dist/button-group-CEdMwvJJ.js.map +0 -1
- package/dist/is-dark-mode-DicqGkCJ.js.map +0 -1
- package/dist/navigation-rail-Lxetd5-Z.js.map +0 -1
- package/dist/src/menu/menu/MenuSurfaceController.d.ts +0 -18
- package/src/menu/menu/MenuSurfaceController.ts +0 -61
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { Workflow, WorkflowNode, ValidationError } from './types.js';
|
|
2
|
+
import {
|
|
3
|
+
findNodeById,
|
|
4
|
+
getAllNodes,
|
|
5
|
+
getNodePath,
|
|
6
|
+
isDescendant,
|
|
7
|
+
} from './workflow-utils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Workflow validation - checks for common errors and inconsistencies
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class WorkflowValidator {
|
|
14
|
+
/**
|
|
15
|
+
* Validate entire workflow
|
|
16
|
+
*/
|
|
17
|
+
static validate(workflow: Workflow): ValidationError[] {
|
|
18
|
+
const errors: ValidationError[] = [];
|
|
19
|
+
|
|
20
|
+
// Check root node exists and is a trigger
|
|
21
|
+
if (!workflow.nodes) {
|
|
22
|
+
errors.push({
|
|
23
|
+
nodeId: 'root',
|
|
24
|
+
type: 'orphaned_node',
|
|
25
|
+
message: 'Workflow has no root node',
|
|
26
|
+
severity: 'error',
|
|
27
|
+
});
|
|
28
|
+
return errors;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate all nodes
|
|
32
|
+
const allNodes = getAllNodes(workflow.nodes);
|
|
33
|
+
|
|
34
|
+
// Check for circular loops
|
|
35
|
+
errors.push(...this._checkCircularLoops(workflow.nodes, allNodes));
|
|
36
|
+
|
|
37
|
+
// Check for orphaned nodes
|
|
38
|
+
errors.push(...this._checkOrphanedNodes(workflow.nodes, allNodes));
|
|
39
|
+
|
|
40
|
+
// Check valid branches
|
|
41
|
+
errors.push(...this._checkValidBranches(workflow.nodes, allNodes));
|
|
42
|
+
|
|
43
|
+
// Check missing targets
|
|
44
|
+
errors.push(...this._checkMissingTargets(workflow, allNodes));
|
|
45
|
+
|
|
46
|
+
// Check invalid fork/join pairs
|
|
47
|
+
errors.push(...this._checkForkJoinPairs(workflow.nodes, allNodes));
|
|
48
|
+
|
|
49
|
+
return errors;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Detect circular loop references
|
|
54
|
+
* A loop_end cannot point to a node that is its own descendant (after the loop_start)
|
|
55
|
+
*/
|
|
56
|
+
private static _checkCircularLoops(
|
|
57
|
+
rootNode: WorkflowNode,
|
|
58
|
+
allNodes: WorkflowNode[]
|
|
59
|
+
): ValidationError[] {
|
|
60
|
+
const errors: ValidationError[] = [];
|
|
61
|
+
|
|
62
|
+
for (const node of allNodes) {
|
|
63
|
+
// Only check loop_end nodes
|
|
64
|
+
if (node.type !== 'loop_end') continue;
|
|
65
|
+
|
|
66
|
+
const targetId = node.target_id;
|
|
67
|
+
if (!targetId) continue;
|
|
68
|
+
|
|
69
|
+
// Check if target exists
|
|
70
|
+
const targetNode = findNodeById(rootNode, targetId);
|
|
71
|
+
if (!targetNode) continue;
|
|
72
|
+
|
|
73
|
+
// If loop_end is a descendant of its target, it's circular
|
|
74
|
+
if (isDescendant(rootNode, targetId, node.id)) {
|
|
75
|
+
// This is the intended behavior - loop_end should be a descendant of loop_start
|
|
76
|
+
// So this is actually valid. Skip this check.
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// However, if the loop_end's target is a descendant of the loop_end, that's circular
|
|
80
|
+
if (isDescendant(rootNode, node.id, targetId)) {
|
|
81
|
+
errors.push({
|
|
82
|
+
nodeId: node.id,
|
|
83
|
+
type: 'circular_loop',
|
|
84
|
+
message: `Loop cannot point to a node (${targetId}) that executes after the loop_end`,
|
|
85
|
+
severity: 'error',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check for orphaned nodes (not reachable from root)
|
|
95
|
+
*/
|
|
96
|
+
private static _checkOrphanedNodes(
|
|
97
|
+
rootNode: WorkflowNode,
|
|
98
|
+
allNodes: WorkflowNode[]
|
|
99
|
+
): ValidationError[] {
|
|
100
|
+
const errors: ValidationError[] = [];
|
|
101
|
+
const paths = new Map<string, string[]>();
|
|
102
|
+
|
|
103
|
+
for (const node of allNodes) {
|
|
104
|
+
const path = getNodePath(rootNode, node.id);
|
|
105
|
+
if (path.length === 0 && node.id !== rootNode.id) {
|
|
106
|
+
errors.push({
|
|
107
|
+
nodeId: node.id,
|
|
108
|
+
type: 'orphaned_node',
|
|
109
|
+
message: `Node "${node.label}" is not reachable from the root trigger`,
|
|
110
|
+
severity: 'error',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
paths.set(node.id, path);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check nodes in branches - all branch paths must be reachable
|
|
117
|
+
// This is typically valid by structure, but warn if branch has no exit
|
|
118
|
+
for (const node of allNodes) {
|
|
119
|
+
if (!node.branches) continue;
|
|
120
|
+
|
|
121
|
+
for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
|
|
122
|
+
if (branchNodes.length === 0) {
|
|
123
|
+
errors.push({
|
|
124
|
+
nodeId: node.id,
|
|
125
|
+
type: 'invalid_branch',
|
|
126
|
+
message: `Branch "${branchKey}" is empty - no nodes to execute`,
|
|
127
|
+
severity: 'warning',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check that decision nodes have valid branches
|
|
138
|
+
*/
|
|
139
|
+
private static _checkValidBranches(
|
|
140
|
+
rootNode: WorkflowNode,
|
|
141
|
+
allNodes: WorkflowNode[]
|
|
142
|
+
): ValidationError[] {
|
|
143
|
+
const errors: ValidationError[] = [];
|
|
144
|
+
|
|
145
|
+
for (const node of allNodes) {
|
|
146
|
+
if (node.type !== 'decision') continue;
|
|
147
|
+
|
|
148
|
+
if (!node.branches) {
|
|
149
|
+
errors.push({
|
|
150
|
+
nodeId: node.id,
|
|
151
|
+
type: 'invalid_branch',
|
|
152
|
+
message: `Decision node "${node.label}" has no branches defined`,
|
|
153
|
+
severity: 'error',
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Standard decision should have "yes" and "no"
|
|
159
|
+
const branchKeys = Object.keys(node.branches);
|
|
160
|
+
if (!branchKeys.includes('yes') || !branchKeys.includes('no')) {
|
|
161
|
+
errors.push({
|
|
162
|
+
nodeId: node.id,
|
|
163
|
+
type: 'invalid_branch',
|
|
164
|
+
message: `Decision node "${node.label}" should have "yes" and "no" branches`,
|
|
165
|
+
severity: 'warning',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for empty branches
|
|
170
|
+
for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
|
|
171
|
+
if (branchNodes.length === 0) {
|
|
172
|
+
errors.push({
|
|
173
|
+
nodeId: node.id,
|
|
174
|
+
type: 'invalid_branch',
|
|
175
|
+
message: `Decision branch "${branchKey}" is empty`,
|
|
176
|
+
severity: 'warning',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check that loop_end nodes reference valid loop_start nodes
|
|
187
|
+
*/
|
|
188
|
+
private static _checkMissingTargets(
|
|
189
|
+
workflow: Workflow,
|
|
190
|
+
allNodes: WorkflowNode[]
|
|
191
|
+
): ValidationError[] {
|
|
192
|
+
const errors: ValidationError[] = [];
|
|
193
|
+
|
|
194
|
+
for (const node of allNodes) {
|
|
195
|
+
if (node.type === 'loop_end') {
|
|
196
|
+
if (!node.target_id) {
|
|
197
|
+
errors.push({
|
|
198
|
+
nodeId: node.id,
|
|
199
|
+
type: 'missing_target',
|
|
200
|
+
message: `Loop end "${node.label}" does not specify a target loop_start`,
|
|
201
|
+
severity: 'error',
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
const target = findNodeById(workflow.nodes, node.target_id);
|
|
205
|
+
if (!target || target.type !== 'loop_start') {
|
|
206
|
+
errors.push({
|
|
207
|
+
nodeId: node.id,
|
|
208
|
+
type: 'missing_target',
|
|
209
|
+
message: `Loop end "${node.label}" references non-existent or non-loop_start node "${node.target_id}"`,
|
|
210
|
+
severity: 'error',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return errors;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check that fork nodes have corresponding join nodes
|
|
222
|
+
*/
|
|
223
|
+
private static _checkForkJoinPairs(
|
|
224
|
+
rootNode: WorkflowNode,
|
|
225
|
+
allNodes: WorkflowNode[]
|
|
226
|
+
): ValidationError[] {
|
|
227
|
+
const errors: ValidationError[] = [];
|
|
228
|
+
|
|
229
|
+
for (const node of allNodes) {
|
|
230
|
+
if (node.type === 'fork') {
|
|
231
|
+
if (!node.join) {
|
|
232
|
+
errors.push({
|
|
233
|
+
nodeId: node.id,
|
|
234
|
+
type: 'invalid_fork_join',
|
|
235
|
+
message: `Fork node "${node.label}" does not have a corresponding join node`,
|
|
236
|
+
severity: 'error',
|
|
237
|
+
});
|
|
238
|
+
} else if (node.join.type !== 'join') {
|
|
239
|
+
errors.push({
|
|
240
|
+
nodeId: node.id,
|
|
241
|
+
type: 'invalid_fork_join',
|
|
242
|
+
message: `Fork node "${node.label}" join is not a join node`,
|
|
243
|
+
severity: 'error',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!node.tasks || node.tasks.length === 0) {
|
|
248
|
+
errors.push({
|
|
249
|
+
nodeId: node.id,
|
|
250
|
+
type: 'invalid_fork_join',
|
|
251
|
+
message: `Fork node "${node.label}" has no parallel tasks`,
|
|
252
|
+
severity: 'warning',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return errors;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if workflow would create a valid execution path
|
|
263
|
+
*/
|
|
264
|
+
static isExecutable(workflow: Workflow): boolean {
|
|
265
|
+
const errors = this.validate(workflow);
|
|
266
|
+
return errors.filter((e) => e.severity === 'error').length === 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get validation warnings only
|
|
271
|
+
*/
|
|
272
|
+
static getWarnings(workflow: Workflow): ValidationError[] {
|
|
273
|
+
const errors = this.validate(workflow);
|
|
274
|
+
return errors.filter((e) => e.severity === 'warning');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get validation errors only
|
|
279
|
+
*/
|
|
280
|
+
static getErrors(workflow: Workflow): ValidationError[] {
|
|
281
|
+
const errors = this.validate(workflow);
|
|
282
|
+
return errors.filter((e) => e.severity === 'error');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { Workflow, WorkflowNode } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow utility functions for tree traversal and manipulation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deep clone a workflow to ensure immutability
|
|
9
|
+
*/
|
|
10
|
+
export function cloneWorkflow(workflow: Workflow): Workflow {
|
|
11
|
+
return JSON.parse(JSON.stringify(workflow));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Deep clone a workflow node
|
|
16
|
+
*/
|
|
17
|
+
export function cloneNode(node: WorkflowNode): WorkflowNode {
|
|
18
|
+
return JSON.parse(JSON.stringify(node));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find a node by ID anywhere in the workflow tree
|
|
23
|
+
*/
|
|
24
|
+
export function findNodeById(
|
|
25
|
+
node: WorkflowNode,
|
|
26
|
+
id: string
|
|
27
|
+
): WorkflowNode | null {
|
|
28
|
+
if (node.id === id) return node;
|
|
29
|
+
|
|
30
|
+
// Search children
|
|
31
|
+
if (node.children) {
|
|
32
|
+
for (const child of node.children) {
|
|
33
|
+
const found = findNodeById(child, id);
|
|
34
|
+
if (found) return found;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Search tasks
|
|
39
|
+
if (node.tasks) {
|
|
40
|
+
for (const task of node.tasks) {
|
|
41
|
+
const found = findNodeById(task, id);
|
|
42
|
+
if (found) return found;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Search branches
|
|
47
|
+
if (node.branches) {
|
|
48
|
+
for (const branchNodes of Object.values(node.branches)) {
|
|
49
|
+
for (const branchNode of branchNodes) {
|
|
50
|
+
const found = findNodeById(branchNode, id);
|
|
51
|
+
if (found) return found;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Search join
|
|
57
|
+
if (node.join) {
|
|
58
|
+
const found = findNodeById(node.join, id);
|
|
59
|
+
if (found) return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Remove a node by ID from the workflow tree
|
|
67
|
+
*/
|
|
68
|
+
export function removeNodeById(
|
|
69
|
+
node: WorkflowNode,
|
|
70
|
+
id: string
|
|
71
|
+
): WorkflowNode {
|
|
72
|
+
const result = cloneNode(node);
|
|
73
|
+
|
|
74
|
+
// Remove from children
|
|
75
|
+
if (result.children) {
|
|
76
|
+
result.children = result.children.filter((child) => {
|
|
77
|
+
if (child.id === id) return false;
|
|
78
|
+
removeNodeById(child, id);
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Remove from tasks
|
|
84
|
+
if (result.tasks) {
|
|
85
|
+
result.tasks = result.tasks.filter((task) => {
|
|
86
|
+
if (task.id === id) return false;
|
|
87
|
+
removeNodeById(task, id);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remove from branches
|
|
93
|
+
if (result.branches) {
|
|
94
|
+
for (const [branchKey, branchNodes] of Object.entries(result.branches)) {
|
|
95
|
+
result.branches[branchKey] = branchNodes.filter((branchNode) => {
|
|
96
|
+
if (branchNode.id === id) return false;
|
|
97
|
+
removeNodeById(branchNode, id);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Recursively clean empty nodes
|
|
104
|
+
for (const branchNode of result.children || []) {
|
|
105
|
+
removeNodeById(branchNode, id);
|
|
106
|
+
}
|
|
107
|
+
for (const taskNode of result.tasks || []) {
|
|
108
|
+
removeNodeById(taskNode, id);
|
|
109
|
+
}
|
|
110
|
+
for (const branchNodes of Object.values(result.branches || {})) {
|
|
111
|
+
for (const branchNode of branchNodes) {
|
|
112
|
+
removeNodeById(branchNode, id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Insert a node into the workflow tree at a specific location
|
|
121
|
+
*/
|
|
122
|
+
export function insertNodeIntoWorkflow(
|
|
123
|
+
parent: WorkflowNode,
|
|
124
|
+
nodeToInsert: WorkflowNode,
|
|
125
|
+
connectionType: 'child' | 'branch' | 'task' = 'child',
|
|
126
|
+
branchKey?: string
|
|
127
|
+
): void {
|
|
128
|
+
switch (connectionType) {
|
|
129
|
+
case 'child':
|
|
130
|
+
if (!parent.children) parent.children = [];
|
|
131
|
+
parent.children.push(cloneNode(nodeToInsert));
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'task':
|
|
135
|
+
if (!parent.tasks) parent.tasks = [];
|
|
136
|
+
parent.tasks.push(cloneNode(nodeToInsert));
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case 'branch':
|
|
140
|
+
if (!parent.branches) parent.branches = {};
|
|
141
|
+
if (!branchKey) branchKey = 'default';
|
|
142
|
+
if (!parent.branches[branchKey]) parent.branches[branchKey] = [];
|
|
143
|
+
parent.branches[branchKey].push(cloneNode(nodeToInsert));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Collect all nodes in the workflow (depth-first)
|
|
150
|
+
*/
|
|
151
|
+
export function getAllNodes(node: WorkflowNode): WorkflowNode[] {
|
|
152
|
+
const result: WorkflowNode[] = [node];
|
|
153
|
+
|
|
154
|
+
if (node.children) {
|
|
155
|
+
for (const child of node.children) {
|
|
156
|
+
result.push(...getAllNodes(child));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (node.tasks) {
|
|
161
|
+
for (const task of node.tasks) {
|
|
162
|
+
result.push(...getAllNodes(task));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (node.branches) {
|
|
167
|
+
for (const branchNodes of Object.values(node.branches)) {
|
|
168
|
+
for (const branchNode of branchNodes) {
|
|
169
|
+
result.push(...getAllNodes(branchNode));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (node.join) {
|
|
175
|
+
result.push(...getAllNodes(node.join));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get all parent node IDs for a given node (path from root to node)
|
|
183
|
+
*/
|
|
184
|
+
export function getNodePath(
|
|
185
|
+
rootNode: WorkflowNode,
|
|
186
|
+
targetId: string
|
|
187
|
+
): string[] {
|
|
188
|
+
const path: string[] = [];
|
|
189
|
+
|
|
190
|
+
function traverse(node: WorkflowNode): boolean {
|
|
191
|
+
path.push(node.id);
|
|
192
|
+
|
|
193
|
+
if (node.id === targetId) return true;
|
|
194
|
+
|
|
195
|
+
// Search children
|
|
196
|
+
if (node.children) {
|
|
197
|
+
for (const child of node.children) {
|
|
198
|
+
if (traverse(child)) return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Search tasks
|
|
203
|
+
if (node.tasks) {
|
|
204
|
+
for (const task of node.tasks) {
|
|
205
|
+
if (traverse(task)) return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Search branches
|
|
210
|
+
if (node.branches) {
|
|
211
|
+
for (const branchNodes of Object.values(node.branches)) {
|
|
212
|
+
for (const branchNode of branchNodes) {
|
|
213
|
+
if (traverse(branchNode)) return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Search join
|
|
219
|
+
if (node.join) {
|
|
220
|
+
if (traverse(node.join)) return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
path.pop();
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
traverse(rootNode);
|
|
228
|
+
return path;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if a node is a descendant of another node
|
|
233
|
+
*/
|
|
234
|
+
export function isDescendant(
|
|
235
|
+
rootNode: WorkflowNode,
|
|
236
|
+
potentialParentId: string,
|
|
237
|
+
nodeId: string
|
|
238
|
+
): boolean {
|
|
239
|
+
const path = getNodePath(rootNode, nodeId);
|
|
240
|
+
return path.includes(potentialParentId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Replace a node in the tree
|
|
245
|
+
*/
|
|
246
|
+
export function replaceNode(
|
|
247
|
+
node: WorkflowNode,
|
|
248
|
+
targetId: string,
|
|
249
|
+
replacement: WorkflowNode
|
|
250
|
+
): WorkflowNode {
|
|
251
|
+
if (node.id === targetId) {
|
|
252
|
+
return cloneNode(replacement);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const result = cloneNode(node);
|
|
256
|
+
|
|
257
|
+
if (result.children) {
|
|
258
|
+
result.children = result.children.map((child) =>
|
|
259
|
+
replaceNode(child, targetId, replacement)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result.tasks) {
|
|
264
|
+
result.tasks = result.tasks.map((task) =>
|
|
265
|
+
replaceNode(task, targetId, replacement)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (result.branches) {
|
|
270
|
+
for (const [branchKey, branchNodes] of Object.entries(result.branches)) {
|
|
271
|
+
result.branches[branchKey] = branchNodes.map((branchNode) =>
|
|
272
|
+
replaceNode(branchNode, targetId, replacement)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (result.join) {
|
|
278
|
+
result.join = replaceNode(result.join, targetId, replacement);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|