This is an automated email from the ASF dual-hosted git repository.

linxinyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 1d2e9cce00 fix(ui): Prevent link delete buttons from overlapping 
(#3602)
1d2e9cce00 is described below

commit 1d2e9cce00eef2c8b44d7914e8d8230b9ba6abfb
Author: Ma77Ball <[email protected]>
AuthorDate: Sun Sep 28 23:03:40 2025 -0700

    fix(ui): Prevent link delete buttons from overlapping (#3602)
    
    # Purpose
    This pull request moves the delete button on links from the sending
    operator to the receiving operator to avoid overlap.
    
    Closes #3074
    
    # Summary of Changes
    - Changed link delete button from tool markup to a linktools.button
    - changed the distance of the info button to -60 to start from the end
    of the link
    - Subscribe to linkView and add info and remove button using the tools
    view
    
    # Demo
    ## Before:
    [Screencast from 2025-09-10
    
11-06-48.webm](https://github.com/user-attachments/assets/6b0706b0-621f-417e-aeeb-5717cc5a6fb2)
    
    ## After:
    [Screencast from 2025-09-10
    
11-01-41.webm](https://github.com/user-attachments/assets/10e0e0f5-1747-49fc-9e98-89e16d1ee7fd)
    
    ---------
    
    Signed-off-by: Ma77Ball <[email protected]>
    Co-authored-by: Xinyuan Lin <[email protected]>
    Co-authored-by: Copilot <[email protected]>
---
 .../workflow-editor/workflow-editor.component.ts   | 134 ++++++++++++++++++---
 .../workspace/service/joint-ui/joint-ui.service.ts |  38 ------
 2 files changed, 120 insertions(+), 52 deletions(-)

diff --git 
a/core/gui/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
 
b/core/gui/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
index 089ea9eead..e46bb9acec 100644
--- 
a/core/gui/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
+++ 
b/core/gui/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
@@ -17,14 +17,19 @@
  * under the License.
  */
 
-import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy } 
from "@angular/core";
+import { OnInit, AfterViewInit, ChangeDetectorRef, Component, ElementRef, 
OnDestroy } from "@angular/core";
 import { fromEvent, merge, Subject } from "rxjs";
 import { NzModalCommentBoxComponent } from 
"./comment-box-modal/nz-modal-comment-box.component";
 import { NzModalRef, NzModalService } from "ng-zorro-antd/modal";
 import { DragDropService } from "../../service/drag-drop/drag-drop.service";
 import { DynamicSchemaService } from 
"../../service/dynamic-schema/dynamic-schema.service";
 import { ExecuteWorkflowService } from 
"../../service/execute-workflow/execute-workflow.service";
-import { fromJointPaperEvent, JointUIService, linkPathStrokeColor } from 
"../../service/joint-ui/joint-ui.service";
+import {
+  deleteButtonPath,
+  fromJointPaperEvent,
+  JointUIService,
+  linkPathStrokeColor,
+} from "../../service/joint-ui/joint-ui.service";
 import { ValidationWorkflowService } from 
"../../service/validation/validation-workflow.service";
 import { WorkflowActionService } from 
"../../service/workflow-graph/model/workflow-action.service";
 import { WorkflowStatusService } from 
"../../service/workflow-status/workflow-status.service";
@@ -82,7 +87,7 @@ export const MAIN_CANVAS = {
   templateUrl: "workflow-editor.component.html",
   styleUrls: ["workflow-editor.component.scss"],
 })
-export class WorkflowEditorComponent implements AfterViewInit, OnDestroy {
+export class WorkflowEditorComponent implements OnInit, AfterViewInit, 
OnDestroy {
   editor!: HTMLElement;
   editorWrapper!: HTMLElement;
   paper!: joint.dia.Paper;
@@ -91,6 +96,8 @@ export class WorkflowEditorComponent implements 
AfterViewInit, OnDestroy {
   private _onProcessKeyboardActionObservable: Subject<void> = new Subject();
   private wrapper;
   private currentOpenedOperatorID: string | null = null;
+  private removeButton!: new () => joint.linkTools.Button;
+  private breakpointButton!: new () => joint.linkTools.Button;
 
   constructor(
     private workflowActionService: WorkflowActionService,
@@ -114,6 +121,12 @@ export class WorkflowEditorComponent implements 
AfterViewInit, OnDestroy {
     this.wrapper = this.workflowActionService.getJointGraphWrapper();
   }
 
+  ngOnInit(): void {
+    // Cache the tool constructors
+    this.removeButton = WorkflowEditorComponent.getRemoveButton();
+    this.breakpointButton = WorkflowEditorComponent.getBreakpointButton();
+  }
+
   /**
    * This function is provided to JointJS to disallow links starting from an 
in port.
    *
@@ -1086,18 +1099,17 @@ export class WorkflowEditorComponent implements 
AfterViewInit, OnDestroy {
     fromJointPaperEvent(this.paper, "link:mouseenter")
       .pipe(map(value => value[0]))
       .pipe(untilDestroyed(this))
-      .subscribe(elementView => {
+      .subscribe(linkView => {
+        // Create an array to hold the tools
+        const tools: joint.dia.ToolView[] = [new this.removeButton()];
+
+        // If breakpoints are enabled, also add the breakpoint button
         if (this.config.env.linkBreakpointEnabled) {
-          this.paper.getModelById(elementView.model.id).attr({
-            ".tool-remove": { display: "block" },
-          });
-          
this.paper.getModelById(elementView.model.id).findView(this.paper).showTools();
-        } else {
-          // only display the delete button
-          this.paper.getModelById(elementView.model.id).attr({
-            ".tool-remove": { display: "block" },
-          });
+          tools.push(new this.breakpointButton());
         }
+
+        const toolsView = new joint.dia.ToolsView({ tools });
+        linkView.addTools(toolsView);
       });
 
     /**
@@ -1139,7 +1151,7 @@ export class WorkflowEditorComponent implements 
AfterViewInit, OnDestroy {
       .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, 
untilDestroyed(this))
       .subscribe(link => {
         const linkView = link.findView(this.paper);
-        const breakpointButtonTool = this.jointUIService.getBreakpointButton();
+        const breakpointButtonTool = this.breakpointButton;
         const breakpointButton = new breakpointButtonTool();
         const toolsView = new joint.dia.ToolsView({
           name: "basic-tools",
@@ -1362,4 +1374,98 @@ export class WorkflowEditorComponent implements 
AfterViewInit, OnDestroy {
         this.paper.translate(-targetCoord.x, -targetCoord.y);
       });
   }
+
+  /**
+   * Info button on link between operator shown when user hovers over links
+   */
+  private static getBreakpointButton(): new () => joint.linkTools.Button {
+    return joint.linkTools.Button.extend({
+      name: "info-button",
+      options: {
+        markup: [
+          {
+            tagName: "circle",
+            selector: "info-button",
+            attributes: {
+              r: 10,
+              fill: "#001DFF",
+              cursor: "pointer",
+            },
+          },
+          {
+            tagName: "path",
+            selector: "icon",
+            attributes: {
+              d: "M -2 4 2 4 M 0 3 0 0 M -2 -1 1 -1 M -1 -4 1 -4",
+              fill: "none",
+              stroke: "#FFFFFF",
+              "stroke-width": 2,
+              "pointer-events": "none",
+            },
+          },
+        ],
+        distance: -60,
+        offset: 0,
+        action: function (event: JQuery.Event, linkView: joint.dia.LinkView) {
+          // when this button is clicked, it triggers an joint paper event
+          if (linkView.paper) {
+            linkView.paper.trigger("tool:breakpoint", linkView, event);
+          }
+        },
+      },
+    });
+  }
+
+  /**
+   * Remove button on link between operator shown when user hovers over links
+   */
+  private static RemoveButton: new () => joint.linkTools.Button;
+
+  private static getRemoveButton(): new () => joint.linkTools.Button {
+    // Check if the class has already been created.
+    if (!WorkflowEditorComponent.RemoveButton) {
+      // If not, create it once and store it in the static property.
+      WorkflowEditorComponent.RemoveButton = joint.linkTools.Button.extend({
+        name: "remove-button",
+        options: {
+          markup: [
+            {
+              tagName: "circle",
+              selector: "button",
+              attributes: {
+                r: 10,
+                fill: "none",
+                stroke: "#D8656A",
+                "stroke-width": 2,
+                "pointer-events": "visibleStroke",
+                cursor: "pointer",
+              },
+            },
+            {
+              tagName: "path",
+              selector: "icon",
+              attributes: {
+                d: "M -4 -4 L 4 4 M 4 -4 L -4 4",
+                fill: "none",
+                stroke: "#D8656A",
+                "stroke-width": 2,
+                "stroke-linecap": "round",
+                "pointer-events": "none",
+              },
+            },
+          ],
+          distance: -90,
+          offset: 0,
+          action: function (evt: JQuery.Event, linkView: joint.dia.LinkView) {
+            if (linkView.paper) {
+              linkView.paper.trigger("tool:remove", linkView, evt);
+            }
+          },
+        },
+      });
+    }
+
+    // Return the cached class.
+    return WorkflowEditorComponent.RemoveButton;
+  }
 }
diff --git a/core/gui/src/app/workspace/service/joint-ui/joint-ui.service.ts 
b/core/gui/src/app/workspace/service/joint-ui/joint-ui.service.ts
index 700594ab2d..ff0c930ab7 100644
--- a/core/gui/src/app/workspace/service/joint-ui/joint-ui.service.ts
+++ b/core/gui/src/app/workspace/service/joint-ui/joint-ui.service.ts
@@ -472,44 +472,6 @@ export class JointUIService {
     
jointPaper.getModelById(operator.operatorID).attr(`.${operatorNameClass}/text`, 
displayName);
   }
 
-  public getBreakpointButton(): new () => joint.linkTools.Button {
-    return joint.linkTools.Button.extend({
-      name: "info-button",
-      options: {
-        markup: [
-          {
-            tagName: "circle",
-            selector: "info-button",
-            attributes: {
-              r: 10,
-              fill: "#001DFF",
-              cursor: "pointer",
-            },
-          },
-          {
-            tagName: "path",
-            selector: "icon",
-            attributes: {
-              d: "M -2 4 2 4 M 0 3 0 0 M -2 -1 1 -1 M -1 -4 1 -4",
-              fill: "none",
-              stroke: "#FFFFFF",
-              "stroke-width": 2,
-              "pointer-events": "none",
-            },
-          },
-        ],
-        distance: 60,
-        offset: 0,
-        action: function (event: JQuery.Event, linkView: joint.dia.LinkView) {
-          // when this button is clicked, it triggers an joint paper event
-          if (linkView.paper) {
-            linkView.paper.trigger("tool:breakpoint", linkView, event);
-          }
-        },
-      },
-    });
-  }
-
   public getCommentElement(commentBox: CommentBox): joint.dia.Element {
     const basic = new joint.shapes.standard.Rectangle();
     if (commentBox.commentBoxPosition) 
basic.position(commentBox.commentBoxPosition.x, 
commentBox.commentBoxPosition.y);

Reply via email to