mcgilman commented on code in PR #8762:
URL: https://github.com/apache/nifi/pull/8762#discussion_r1594086173


##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts:
##########
@@ -231,6 +231,33 @@ export class CanvasUtils {
         return this.isNotRootGroup() && selection.empty();
     }
 
+    /**
+     * Determines if the specified selection is alignable (in a single action).
+     *
+     * @param {selection} selection     The selection
+     * @returns {boolean}
+     */
+    public canAlign(selection: any) {
+        let canAlign = true;
+
+        // determine if the current selection is entirely connections
+        const selectedConnections = selection.filter((d: any) => {
+            return d.type == 'Connection';

Review Comment:
   Can we use `ComponentType.Connection` here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: 0,
+                                y: center - (d.position.y + 
d.dimensions.height / 2)
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.y !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection
+                                            const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                                connectionSelection.datum(),
+                                                delta
+                                            );
+                                            if (connectionUpdate !== null) {
+                                                updates.set(connection.id, 
connectionUpdate);
+                                            }
+                                        }
+                                    }
+                                });
+
+                                updates.set(d.id, 
this.draggableBehavior.updateComponentPosition(d, delta));
+                            }
+                        }
+                    });
+
+                    if (updates.size > 0) {
+                        // dispatch the position updates
+                        this.store.dispatch(
+                            updatePositions({
+                                request: {
+                                    requestId: 
this.updateConnectionRequestId++,
+                                    componentUpdates: 
Array.from(updates.values()),
+                                    connectionUpdates: 
Array.from(updates.values())
+                                }
+                            })
+                        );
+                    }
                 }
             },
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center',
                 text: 'Vertically',
-                action: () => {
-                    // TODO - alignVertical
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minX = 0;
+                    let maxX = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {

Review Comment:
   Can we use `ComponentType.Connection` here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: 0,
+                                y: center - (d.position.y + 
d.dimensions.height / 2)
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.y !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection
+                                            const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                                connectionSelection.datum(),
+                                                delta
+                                            );
+                                            if (connectionUpdate !== null) {
+                                                updates.set(connection.id, 
connectionUpdate);
+                                            }
+                                        }
+                                    }
+                                });
+
+                                updates.set(d.id, 
this.draggableBehavior.updateComponentPosition(d, delta));
+                            }
+                        }
+                    });
+
+                    if (updates.size > 0) {
+                        // dispatch the position updates
+                        this.store.dispatch(
+                            updatePositions({
+                                request: {
+                                    requestId: 
this.updateConnectionRequestId++,
+                                    componentUpdates: 
Array.from(updates.values()),
+                                    connectionUpdates: 
Array.from(updates.values())
+                                }
+                            })
+                        );
+                    }
                 }
             },
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center',
                 text: 'Vertically',
-                action: () => {
-                    // TODO - alignVertical
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minX = 0;
+                    let maxX = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minX === 0 || d.position.x < minX) {
+                                minX = d.position.x;
+                            }
+                            const componentMaxX = d.position.x + 
d.dimensions.width;
+                            if (maxX === 0 || componentMaxX > maxX) {
+                                maxX = componentMaxX;
+                            }
+                        }
+                    });
+
+                    const center = (minX + maxX) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: center - (d.position.x + d.dimensions.width 
/ 2),
+                                y: 0
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.x !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection
+                                            const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                                connectionSelection.datum(),
+                                                delta
+                                            );
+                                            if (connectionUpdate !== null) {
+                                                updates.set(connection.id, 
connectionUpdate);
+                                            }
+                                        }
+                                    }
+                                });
+
+                                updates.set(d.id, 
this.draggableBehavior.updateComponentPosition(d, delta));
+                            }
+                        }
+                    });
+
+                    if (updates.size > 0) {
+                        // dispatch the position updates
+                        this.store.dispatch(
+                            updatePositions({
+                                request: {
+                                    requestId: 
this.updateConnectionRequestId++,
+                                    componentUpdates: 
Array.from(updates.values()),
+                                    connectionUpdates: 
Array.from(updates.values())

Review Comment:
   The contents of `updates` which can contain both components and connections 
are supplied to both `componentUpdates` and `connectionUpdates`. Updates for 
connections should be supplied to `connectionUpdates` and components (non 
connections) should be supplied to `componentUpdates`.



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: 0,
+                                y: center - (d.position.y + 
d.dimensions.height / 2)
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.y !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection

Review Comment:
   I'm struggling with this logic and the comment. What are we attempting to 
check here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: 0,
+                                y: center - (d.position.y + 
d.dimensions.height / 2)
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.y !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection
+                                            const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                                connectionSelection.datum(),
+                                                delta
+                                            );
+                                            if (connectionUpdate !== null) {
+                                                updates.set(connection.id, 
connectionUpdate);
+                                            }
+                                        }
+                                    }
+                                });
+
+                                updates.set(d.id, 
this.draggableBehavior.updateComponentPosition(d, delta));
+                            }
+                        }
+                    });
+
+                    if (updates.size > 0) {
+                        // dispatch the position updates
+                        this.store.dispatch(
+                            updatePositions({
+                                request: {
+                                    requestId: 
this.updateConnectionRequestId++,
+                                    componentUpdates: 
Array.from(updates.values()),
+                                    connectionUpdates: 
Array.from(updates.values())
+                                }
+                            })
+                        );
+                    }
                 }
             },
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center',
                 text: 'Vertically',
-                action: () => {
-                    // TODO - alignVertical
+                action: (selection: any) => {
+                    const updates = new Map();

Review Comment:
   Can we use the appropriate generic type here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();

Review Comment:
   Since use the appropriate generic type here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: 0,
+                                y: center - (d.position.y + 
d.dimensions.height / 2)
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.y !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection
+                                            const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                                connectionSelection.datum(),
+                                                delta
+                                            );
+                                            if (connectionUpdate !== null) {
+                                                updates.set(connection.id, 
connectionUpdate);
+                                            }
+                                        }
+                                    }
+                                });
+
+                                updates.set(d.id, 
this.draggableBehavior.updateComponentPosition(d, delta));
+                            }
+                        }
+                    });
+
+                    if (updates.size > 0) {
+                        // dispatch the position updates
+                        this.store.dispatch(
+                            updatePositions({
+                                request: {
+                                    requestId: 
this.updateConnectionRequestId++,
+                                    componentUpdates: 
Array.from(updates.values()),
+                                    connectionUpdates: 
Array.from(updates.values())
+                                }
+                            })
+                        );
+                    }
                 }
             },
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center',
                 text: 'Vertically',
-                action: () => {
-                    // TODO - alignVertical
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minX = 0;
+                    let maxX = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minX === 0 || d.position.x < minX) {
+                                minX = d.position.x;
+                            }
+                            const componentMaxX = d.position.x + 
d.dimensions.width;
+                            if (maxX === 0 || componentMaxX > maxX) {
+                                maxX = componentMaxX;
+                            }
+                        }
+                    });
+
+                    const center = (minX + maxX) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {

Review Comment:
   Can we use `ComponentType.Connection` here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -88,9 +89,13 @@ import { navigateToComponentDocumentation } from 
'../../../state/documentation/d
 import * as d3 from 'd3';
 import { Client } from '../../../service/client.service';
 import { CanvasView } from './canvas-view.service';
+import * as FlowActions from '../state/flow/flow.actions';
+import { DraggableBehavior } from './behavior/draggable-behavior.service';
 
 @Injectable({ providedIn: 'root' })
 export class CanvasContextMenu implements ContextMenuDefinitionProvider {
+    private updateConnectionRequestId = 0;

Review Comment:
   Can we rename this to something more meaningful? Maybe `alignmentRequestId` 
or `updatePositionRequestId`.
   
   I did notice that it's the same name from `Draggable` and that the name 
could probably be updated there as well. Something like 
`updatePositionRequestId` would also make sense there.



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {

Review Comment:
   Can we use `ComponentType.Connection` here?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -321,24 +326,193 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         menuItems: [
             {
                 condition: (selection: any) => {
-                    // TODO - canAlign
-                    return false;
+                    return this.canvasUtils.canAlign(selection);
                 },
                 clazz: 'fa fa-align-center fa-rotate-90',
                 text: 'Horizontally',
-                action: () => {
-                    // TODO - alignHorizontal
+                action: (selection: any) => {
+                    const updates = new Map();
+
+                    // determine the extent
+                    let minY: number = 0,
+                        maxY: number = 0;
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            if (minY === 0 || d.position.y < minY) {
+                                minY = d.position.y;
+                            }
+                            const componentMaxY = d.position.y + 
d.dimensions.height;
+                            if (maxY === 0 || componentMaxY > maxY) {
+                                maxY = componentMaxY;
+                            }
+                        }
+                    });
+
+                    const center = (minY + maxY) / 2;
+
+                    // align all components with top most component
+                    selection.each((d: any) => {
+                        if (d.type !== 'Connection') {
+                            const delta = {
+                                x: 0,
+                                y: center - (d.position.y + 
d.dimensions.height / 2)
+                            };
+
+                            // if this component is already centered, no need 
to updated it
+                            if (delta.y !== 0) {
+                                // consider any connections
+                                const connections = 
this.canvasUtils.getComponentConnections(d.id);
+
+                                connections.forEach((connection: any) => {
+                                    const connectionSelection = 
d3.select('#id-' + connection.id);
+
+                                    if (
+                                        !updates.has(connection.id) &&
+                                        
this.canvasUtils.getConnectionSourceComponentId(connection) ===
+                                            
this.canvasUtils.getConnectionDestinationComponentId(connection)
+                                    ) {
+                                        // this connection is self looping and 
hasn't been updated by the delta yet
+                                        const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                            connectionSelection.datum(),
+                                            delta
+                                        );
+                                        if (connectionUpdate !== null) {
+                                            updates.set(connection.id, 
connectionUpdate);
+                                        }
+                                    } else if (
+                                        !updates.has(connection.id) &&
+                                        
connectionSelection.classed('selected') &&
+                                        
this.canvasUtils.canModify(connectionSelection)
+                                    ) {
+                                        // this is a selected connection that 
hasn't been updated by the delta yet
+                                        if (
+                                            
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
+                                            
!this.canvasUtils.isSourceSelected(connection, selection)
+                                        ) {
+                                            // the connection is either 
outgoing or incoming when the source of the connection is not part of the 
selection
+                                            const connectionUpdate = 
this.draggableBehavior.updateConnectionPosition(
+                                                connectionSelection.datum(),
+                                                delta
+                                            );
+                                            if (connectionUpdate !== null) {
+                                                updates.set(connection.id, 
connectionUpdate);
+                                            }
+                                        }
+                                    }
+                                });
+
+                                updates.set(d.id, 
this.draggableBehavior.updateComponentPosition(d, delta));
+                            }
+                        }
+                    });
+
+                    if (updates.size > 0) {
+                        // dispatch the position updates
+                        this.store.dispatch(
+                            updatePositions({
+                                request: {
+                                    requestId: 
this.updateConnectionRequestId++,
+                                    componentUpdates: 
Array.from(updates.values()),
+                                    connectionUpdates: 
Array.from(updates.values())

Review Comment:
   The contents of `updates` which can contain both components and connections 
are supplied to both `componentUpdates` and `connectionUpdates`. Updates for 
connections should be supplied to `connectionUpdates` and components (non 
connections) should be supplied to `componentUpdates`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to