@@ -46,12 +46,21 @@ import {
46
46
*/
47
47
function addWorkflow ( workflow , gscan , options ) {
48
48
const hierarchical = options . hierarchical || true
49
+ const workflowNode = createWorkflowNode ( workflow , hierarchical )
49
50
if ( hierarchical ) {
50
- const workflowNode = createWorkflowNode ( workflow , hierarchical )
51
- addHierarchicalWorkflow ( workflowNode , gscan . lookup , gscan . tree , options )
51
+ // TBD: We need the leaf node to propagate states, and this is done here since the
52
+ // addHierarchicalWorkflow has recursion. There might be a better way for
53
+ // handling this though?
54
+ let leafNode = workflowNode
55
+ while ( leafNode . children ) {
56
+ // [0] because this is not really a sparse tree, but more like a linked-list since
57
+ // we created the node with createWorkflowNode.
58
+ leafNode = leafNode . children [ 0 ]
59
+ }
60
+ addHierarchicalWorkflow ( workflowNode , leafNode , gscan . lookup , gscan . tree , options )
52
61
} else {
53
- gscan . lookup [ workflow . id ] = workflow
54
- gscan . tree . push ( workflow )
62
+ gscan . lookup [ workflow . id ] = workflowNode
63
+ gscan . tree . push ( workflowNode )
55
64
}
56
65
}
57
66
@@ -60,18 +69,19 @@ function addWorkflow (workflow, gscan, options) {
60
69
* functions of this module). This is required as we apply recursion for adding nodes into the tree,
61
70
* but we replace the tree and pass only a sub-tree.
62
71
*
72
+ * @param workflowOrPart
63
73
* @param workflow
64
74
* @param {Lookup } lookup
65
75
* @param {Array<TreeNode> } tree
66
76
* @param {* } options
67
77
* @private
68
78
*/
69
- function addHierarchicalWorkflow ( workflow , lookup , tree , options ) {
70
- if ( ! lookup [ workflow . id ] ) {
71
- // a new node, let 's add this node and its descendants to the lookup
72
- lookup [ workflow . id ] = workflow
73
- if ( workflow . children ) {
74
- const stack = [ ...workflow . children ]
79
+ function addHierarchicalWorkflow ( workflowOrPart , workflow , lookup , tree , options ) {
80
+ if ( ! lookup [ workflowOrPart . id ] ) {
81
+ // A new node. Let 's add this node and its descendants to the lookup.
82
+ lookup [ workflowOrPart . id ] = workflowOrPart
83
+ if ( workflowOrPart . children ) {
84
+ const stack = [ ...workflowOrPart . children ]
75
85
while ( stack . length ) {
76
86
const currentNode = stack . shift ( )
77
87
lookup [ currentNode . id ] = currentNode
@@ -80,32 +90,38 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
80
90
}
81
91
}
82
92
}
83
- // and now add the top-level node to the tree
84
- // Here we calculate what is the index for this element. If we decide to have ASC and DESC,
85
- // then we just need to invert the location of the element, something like
86
- // `sortedIndex = (array.length - sortedIndex)`.
93
+ // And now add the node to the tree. Here we calculate what is the index for this element.
94
+ // If we decide to have ASC and DESC, then we just need to invert the location of the node,
95
+ // something like `sortedIndex = (array.length - sortedIndex)`.
87
96
const sortedIndex = sortedIndexBy (
88
97
tree ,
89
- workflow ,
98
+ workflowOrPart ,
90
99
( n ) => n . name ,
91
100
sortWorkflowNamePartNodeOrWorkflowNode
92
101
)
93
- tree . splice ( sortedIndex , 0 , workflow )
102
+ tree . splice ( sortedIndex , 0 , workflowOrPart )
94
103
} else {
95
- // we will have to merge the hierarchies
96
- const existingNode = lookup [ workflow . id ]
97
- // TODO: combine states summaries?
104
+ // The node exists in the lookup, so must exist in the tree too. We will have to merge the hierarchies.
105
+ const existingNode = lookup [ workflowOrPart . id ]
98
106
if ( existingNode . children ) {
107
+ // Propagate workflow states to its ancestor.
108
+ if ( workflow . node . latestStateTasks && workflow . node . stateTotals ) {
109
+ existingNode . node . descendantsLatestStateTasks [ workflow . id ] = workflow . node . latestStateTasks
110
+ existingNode . node . descendantsStateTotal [ workflow . id ] = workflow . node . stateTotals
111
+ tallyPropagatedStates ( existingNode . node )
112
+ }
99
113
// Copy array since we will iterate it, and modify existingNode.children
100
114
// (see the tree.splice above.)
101
- const children = [ ...workflow . children ]
115
+ const children = [ ...workflowOrPart . children ]
102
116
for ( const child of children ) {
103
- // Recursion
104
- addHierarchicalWorkflow ( child , lookup , existingNode . children , options )
117
+ // Recursion!
118
+ addHierarchicalWorkflow ( child , workflow , lookup , existingNode . children , options )
105
119
}
106
120
} else {
107
- // Here we have an existing workflow node. Let's merge it.
108
- mergeWith ( existingNode , workflow , mergeWithCustomizer )
121
+ // Here we have an existing workflow node (only child-less). Let's merge it.
122
+ // It should not happen actually, since this is adding a workflow. Maybe throw
123
+ // an error instead?
124
+ mergeWith ( existingNode , workflowOrPart , mergeWithCustomizer )
109
125
}
110
126
}
111
127
}
@@ -129,42 +145,92 @@ function updateWorkflow (workflow, gscan, options) {
129
145
mergeWith ( existingData . node , workflow , mergeWithCustomizer )
130
146
const hierarchical = options . hierarchical || true
131
147
if ( hierarchical ) {
148
+ // But now we need to propagate the states up to its ancestors, if any.
132
149
updateHierarchicalWorkflow ( existingData , gscan . lookup , gscan . tree , options )
133
150
}
134
- // TODO: create workflow hierarchy (from workflow object), then iterate
135
- // it and use lookup to fetch the existing node. Finally, combine
136
- // the gscan states (latestStateTasks & stateTotals).
137
151
Vue . set ( gscan . lookup , existingData . id , existingData )
138
152
}
139
153
140
154
function updateHierarchicalWorkflow ( existingData , lookup , tree , options ) {
141
- // We need to sort its parent again.
142
155
const workflowNameParts = parseWorkflowNameParts ( existingData . id )
156
+ // nodesIds contains the list of GScan tree nodes, with the workflow being a leaf node.
143
157
const nodesIds = getWorkflowNamePartsNodesIds ( workflowNameParts )
144
- // Discard the last since it's the workflow ID that we already have
145
- // in the `existingData` object. Now if not empty, we have our parent.
146
- nodesIds . pop ( )
158
+ const workflowId = nodesIds . pop ( )
147
159
const parentId = nodesIds . length > 0 ? nodesIds . pop ( ) : null
148
160
const parent = parentId ? lookup [ parentId ] : tree
149
161
if ( ! parent ) {
162
+ // This is only possible if the parent was missing from the lookup... Never supposed to happen.
150
163
throw new Error ( `Invalid orphan hierarchical node: ${ existingData . id } ` )
151
164
}
152
165
const siblings = parent . children
153
166
// Where is this node at the moment?
154
167
const currentIndex = siblings . findIndex ( node => node . id === existingData . id )
155
168
// Where should it be now?
156
169
const sortedIndex = sortedIndexBy (
157
- parent . children ,
170
+ siblings ,
158
171
existingData ,
159
172
( n ) => n . name ,
160
173
sortWorkflowNamePartNodeOrWorkflowNode
161
174
)
162
- // If it is not where it is , we need to add it to its correct location.
175
+ // If it is not where it must be , we need to move it to its correct location.
163
176
if ( currentIndex !== sortedIndex ) {
164
- // siblings.splice(currentIndex, 1)
165
- // siblings.splice(sortedIndex, 0, existingData)
166
- Vue . delete ( siblings , currentIndex )
167
- Vue . set ( siblings , sortedIndex , existingData )
177
+ // First we remove the element from where it was.
178
+ siblings . splice ( currentIndex , 1 )
179
+ if ( sortedIndex < currentIndex ) {
180
+ // Now, if we must move the element to a position that is less than where it was, we can simply move it;
181
+ siblings . splice ( sortedIndex , 0 , existingData )
182
+ } else {
183
+ // however, if we are moving it down/later, we must compensate for itself. i.e. the sortedIndex is considering
184
+ // the element itself. So we subtract one from its position.
185
+ siblings . splice ( sortedIndex - 1 , 0 , existingData )
186
+ }
187
+ }
188
+ // Finally, we need to propagate the state totals and latest state tasks,
189
+ // but only if we have a parent (otherwise we are at the top-most level).
190
+ if ( parentId ) {
191
+ const workflow = lookup [ workflowId ]
192
+ const latestStateTasks = workflow . node . latestStateTasks
193
+ const stateTotals = workflow . node . stateTotals
194
+ // Installed workflows do not have any state.
195
+ if ( latestStateTasks && stateTotals ) {
196
+ for ( const parentNodeId of [ ...nodesIds , parentId ] ) {
197
+ const parentNode = lookup [ parentNodeId ]
198
+ if ( parentNode . latestStateTasks && parentNode . stateTotals ) {
199
+ mergeWith ( parentNode . node . descendantsLatestStateTasks [ workflow . id ] , latestStateTasks , mergeWithCustomizer )
200
+ mergeWith ( parentNode . node . descendantsStateTotal [ workflow . id ] , stateTotals , mergeWithCustomizer )
201
+ tallyPropagatedStates ( parentNode . node )
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Computes the latestStateTasks of each node. The latestStateTasks and
210
+ * stateTotals of a workflow-name-part are not reactive, but are calculated
211
+ * based on the values of descendantsLatestStateTasks and descendantsStateTotal,
212
+ * so we need to keep these in sync any time we add or update descendants.
213
+ *
214
+ * @param {WorkflowGraphQLData } node
215
+ */
216
+ function tallyPropagatedStates ( node ) {
217
+ for ( const latestStateTasks of Object . values ( node . descendantsLatestStateTasks ) ) {
218
+ for ( const state of Object . keys ( latestStateTasks ) ) {
219
+ if ( node . latestStateTasks [ state ] ) {
220
+ node . latestStateTasks [ state ] . push ( ...latestStateTasks [ state ] )
221
+ } else {
222
+ Vue . set ( node . latestStateTasks , state , latestStateTasks [ state ] )
223
+ }
224
+ }
225
+ }
226
+ for ( const stateTotals of Object . values ( node . descendantsStateTotal ) ) {
227
+ for ( const state of Object . keys ( stateTotals ) ) {
228
+ if ( node . stateTotals [ state ] ) {
229
+ Vue . set ( node . stateTotals , state , node . stateTotals [ state ] + stateTotals [ state ] )
230
+ } else {
231
+ Vue . set ( node . stateTotals , state , stateTotals [ state ] )
232
+ }
233
+ }
168
234
}
169
235
}
170
236
@@ -205,21 +271,32 @@ function removeHierarchicalWorkflow (workflowId, lookup, tree, options) {
205
271
const workflowNameParts = parseWorkflowNameParts ( workflowId )
206
272
const nodesIds = getWorkflowNamePartsNodesIds ( workflowNameParts )
207
273
// We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
274
+ const removedNodeIds = [ ]
208
275
for ( let i = nodesIds . length - 1 ; i >= 0 ; i -- ) {
209
276
const nodeId = nodesIds [ i ]
210
277
const node = lookup [ nodeId ]
278
+ // If we have children nodes, we MUST not remove the node from the GScan tree, since
279
+ // it contains other workflows branches. Instead, we must only remove the propagated
280
+ // states.
211
281
if ( node . children && node . children . length > 0 ) {
212
- // We stop as soon as we find a node that still has children.
213
- break
214
- }
215
- // Now we can remove the node from the lookup, and from its parents children array.
216
- const previousIndex = i - 1
217
- const parentId = previousIndex >= 0 ? nodesIds [ previousIndex ] : null
218
- if ( parentId && ! lookup [ parentId ] ) {
219
- throw new Error ( `Failed to locate parent ${ parentId } in GScan lookup` )
282
+ // If we pruned a workflow that was installed, these states are undefined!
283
+ if ( node . node . descendantsLatestStateTasks && node . node . descendantsStateTotal ) {
284
+ for ( const removedNodeId of removedNodeIds ) {
285
+ Vue . delete ( node . node . descendantsLatestStateTasks , removedNodeId )
286
+ Vue . delete ( node . node . descendantsStateTotal , removedNodeId )
287
+ }
288
+ }
289
+ } else {
290
+ // Now we can remove the node from the lookup, and from its parents children array.
291
+ const previousIndex = i - 1
292
+ const parentId = previousIndex >= 0 ? nodesIds [ previousIndex ] : null
293
+ if ( parentId && ! lookup [ parentId ] ) {
294
+ throw new Error ( `Failed to locate parent ${ parentId } in GScan lookup` )
295
+ }
296
+ const parentChildren = parentId ? lookup [ parentId ] . children : tree
297
+ removeNode ( nodeId , lookup , parentChildren )
298
+ removedNodeIds . push ( nodeId )
220
299
}
221
- const parentChildren = parentId ? lookup [ parentId ] . children : tree
222
- removeNode ( nodeId , lookup , parentChildren )
223
300
}
224
301
}
225
302
0 commit comments