@@ -46,12 +46,21 @@ import {
4646 */
4747function addWorkflow ( workflow , gscan , options ) {
4848 const hierarchical = options . hierarchical || true
49+ const workflowNode = createWorkflowNode ( workflow , hierarchical )
4950 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 )
5261 } else {
53- gscan . lookup [ workflow . id ] = workflow
54- gscan . tree . push ( workflow )
62+ gscan . lookup [ workflow . id ] = workflowNode
63+ gscan . tree . push ( workflowNode )
5564 }
5665}
5766
@@ -60,18 +69,19 @@ function addWorkflow (workflow, gscan, options) {
6069 * functions of this module). This is required as we apply recursion for adding nodes into the tree,
6170 * but we replace the tree and pass only a sub-tree.
6271 *
72+ * @param workflowOrPart
6373 * @param workflow
6474 * @param {Lookup } lookup
6575 * @param {Array<TreeNode> } tree
6676 * @param {* } options
6777 * @private
6878 */
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 ]
7585 while ( stack . length ) {
7686 const currentNode = stack . shift ( )
7787 lookup [ currentNode . id ] = currentNode
@@ -80,32 +90,38 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
8090 }
8191 }
8292 }
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)`.
8796 const sortedIndex = sortedIndexBy (
8897 tree ,
89- workflow ,
98+ workflowOrPart ,
9099 ( n ) => n . name ,
91100 sortWorkflowNamePartNodeOrWorkflowNode
92101 )
93- tree . splice ( sortedIndex , 0 , workflow )
102+ tree . splice ( sortedIndex , 0 , workflowOrPart )
94103 } 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 ]
98106 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+ }
99113 // Copy array since we will iterate it, and modify existingNode.children
100114 // (see the tree.splice above.)
101- const children = [ ...workflow . children ]
115+ const children = [ ...workflowOrPart . children ]
102116 for ( const child of children ) {
103- // Recursion
104- addHierarchicalWorkflow ( child , lookup , existingNode . children , options )
117+ // Recursion!
118+ addHierarchicalWorkflow ( child , workflow , lookup , existingNode . children , options )
105119 }
106120 } 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 )
109125 }
110126 }
111127}
@@ -129,42 +145,92 @@ function updateWorkflow (workflow, gscan, options) {
129145 mergeWith ( existingData . node , workflow , mergeWithCustomizer )
130146 const hierarchical = options . hierarchical || true
131147 if ( hierarchical ) {
148+ // But now we need to propagate the states up to its ancestors, if any.
132149 updateHierarchicalWorkflow ( existingData , gscan . lookup , gscan . tree , options )
133150 }
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).
137151 Vue . set ( gscan . lookup , existingData . id , existingData )
138152}
139153
140154function updateHierarchicalWorkflow ( existingData , lookup , tree , options ) {
141- // We need to sort its parent again.
142155 const workflowNameParts = parseWorkflowNameParts ( existingData . id )
156+ // nodesIds contains the list of GScan tree nodes, with the workflow being a leaf node.
143157 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 ( )
147159 const parentId = nodesIds . length > 0 ? nodesIds . pop ( ) : null
148160 const parent = parentId ? lookup [ parentId ] : tree
149161 if ( ! parent ) {
162+ // This is only possible if the parent was missing from the lookup... Never supposed to happen.
150163 throw new Error ( `Invalid orphan hierarchical node: ${ existingData . id } ` )
151164 }
152165 const siblings = parent . children
153166 // Where is this node at the moment?
154167 const currentIndex = siblings . findIndex ( node => node . id === existingData . id )
155168 // Where should it be now?
156169 const sortedIndex = sortedIndexBy (
157- parent . children ,
170+ siblings ,
158171 existingData ,
159172 ( n ) => n . name ,
160173 sortWorkflowNamePartNodeOrWorkflowNode
161174 )
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.
163176 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+ }
168234 }
169235}
170236
@@ -205,21 +271,32 @@ function removeHierarchicalWorkflow (workflowId, lookup, tree, options) {
205271 const workflowNameParts = parseWorkflowNameParts ( workflowId )
206272 const nodesIds = getWorkflowNamePartsNodesIds ( workflowNameParts )
207273 // We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
274+ const removedNodeIds = [ ]
208275 for ( let i = nodesIds . length - 1 ; i >= 0 ; i -- ) {
209276 const nodeId = nodesIds [ i ]
210277 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.
211281 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 )
220299 }
221- const parentChildren = parentId ? lookup [ parentId ] . children : tree
222- removeNode ( nodeId , lookup , parentChildren )
223300 }
224301}
225302
0 commit comments