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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5160-a1045ce55e9169deb94f8ae3bf195692ef994ec5
in repository https://gitbox.apache.org/repos/asf/texera.git

commit e6509191d21a4df280bd98b3b73d6dca9f84a761
Author: Jiadong Bai <[email protected]>
AuthorDate: Sat May 30 18:34:24 2026 -0700

    fix(frontend): restore Regions and Status canvas toggles (#5160)
    
    ### What changes were proposed in this PR?
    
    Restores the **Regions** and **Status** toggles under View, both of
    which broke after #4495 switched these elements from JointJS
    `visibility` attributes to CSS classes.
    
    - **Regions**: region visibility is now a shared, persistent flag on
    `JointGraphWrapper` (`setRegionsDisplayed` / `getRegionsDisplayed` /
    stream). The editor applies it to the shared JointJS model both when the
    flag changes and whenever regions are recreated on a
    `RegionUpdateEvent`. Because the model is shared, the toggle drives both
    the main canvas (#5120) and the mini-map (#4027), and it survives the
    region elements being recreated during execution (previously regions
    went hidden mid-run even with the toggle on). The menu toggle just sets
    the flag; the editor owns applying it. The `hide-region` CSS class/SCSS
    rule are removed.
    - **Status** (`workflow-editor.component.scss`,
    `workflow-editor.component.ts`, `menu.component.ts`): re-added the
    `hide-operator-status` SCSS rule (retargeted to the renamed
    `.texera-operator-state` element), added the default class so Status
    starts hidden, and fixed the stale `.operator-status` selector in
    `applyOperatorStatusPosition()`.
    
    Unit tests added for the toggle delegation, status repositioning, the
    default `hide-operator-status` class, and region visibility across
    creation / recreation / toggling.
    
    Here is a demo of the style after fix:
    <img width="1707" height="1162" alt="2026-05-30 11 35 33"
    
src="https://github.com/user-attachments/assets/ae5fc0d3-76f1-4bf8-914e-2644f89b46c2";
    />
    
    
    ### Any related issues, documentation, discussions?
    
    Closes #5120
    
    Regression from #4495.
    
    ### How was this PR tested?
    Manual + unit tests.
    
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 4.8 (1M context)
    
    ---------
    
    Co-authored-by: Bob Bai <[email protected]>
---
 .../component/menu/menu.component.spec.ts          | 83 ++++++++++++++++++++++
 .../app/workspace/component/menu/menu.component.ts | 12 ++--
 .../workflow-editor/workflow-editor.component.scss |  8 +--
 .../workflow-editor.component.spec.ts              | 58 +++++++++++++++
 .../workflow-editor/workflow-editor.component.ts   | 21 +++++-
 .../model/joint-graph-wrapper.spec.ts              | 25 +++++++
 .../workflow-graph/model/joint-graph-wrapper.ts    | 22 +++++-
 7 files changed, 215 insertions(+), 14 deletions(-)

diff --git a/frontend/src/app/workspace/component/menu/menu.component.spec.ts 
b/frontend/src/app/workspace/component/menu/menu.component.spec.ts
index 76de1c1e1b..1449174215 100644
--- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts
+++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts
@@ -526,4 +526,87 @@ describe("MenuComponent", () => {
     expect(config.nzTitle).toBe("Export All Operators Result");
     expect(config.nzData).toEqual(expect.objectContaining({ workflowName: 
"report-wf", sourceTriggered: "menu" }));
   });
+
+  describe("canvas display toggles", () => {
+    // A fake JointJS element that records `attr(path, value)` calls and 
answers `get("type")`.
+    function fakeElement(type: string) {
+      return {
+        type,
+        attrs: {} as Record<string, unknown>,
+        get(key: string) {
+          return key === "type" ? this.type : undefined;
+        },
+        attr: vi.fn(function (this: { attrs: Record<string, unknown> }, path: 
string, value: unknown) {
+          this.attrs[path] = value;
+        }),
+      };
+    }
+
+    // Stubs getJointGraphWrapper() with a paper element + model/graph backed 
by the given elements.
+    function stubWrapper(elements: ReturnType<typeof fakeElement>[]) {
+      const el = document.createElement("div");
+      const wrapper = {
+        mainPaper: { el, model: { getElements: () => elements } },
+        jointGraph: { getElements: () => elements },
+      };
+      vi.spyOn(workflowActionService, 
"getJointGraphWrapper").mockReturnValue(wrapper as any);
+      return el;
+    }
+
+    describe("toggleRegion", () => {
+      it("publishes the displayed flag to the joint graph wrapper when 
enabled", () => {
+        const setSpy = vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"setRegionsDisplayed");
+
+        component.showRegion = true;
+        component.toggleRegion();
+
+        expect(setSpy).toHaveBeenCalledWith(true);
+      });
+
+      it("publishes the displayed flag to the joint graph wrapper when 
disabled", () => {
+        const setSpy = vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"setRegionsDisplayed");
+
+        component.showRegion = false;
+        component.toggleRegion();
+
+        expect(setSpy).toHaveBeenCalledWith(false);
+      });
+    });
+
+    describe("toggleStatus", () => {
+      it("removes hide-operator-status when enabled and repositions the status 
label", () => {
+        const operator = fakeElement("operator");
+        const el = stubWrapper([operator]);
+        el.classList.add("hide-operator-status");
+
+        component.showStatus = true;
+        component.showNumWorkers = false;
+        component.toggleStatus();
+
+        expect(el.classList.contains("hide-operator-status")).toBe(false);
+        
expect(operator.attr).toHaveBeenCalledWith(".texera-operator-state/ref-x", -10);
+        
expect(operator.attr).toHaveBeenCalledWith(".texera-operator-state/ref-y", -35);
+      });
+
+      it("adds hide-operator-status when disabled", () => {
+        const operator = fakeElement("operator");
+        const el = stubWrapper([operator]);
+
+        component.showStatus = false;
+        component.toggleStatus();
+
+        expect(el.classList.contains("hide-operator-status")).toBe(true);
+      });
+
+      it("offsets the status label higher when worker counts are shown", () => 
{
+        const operator = fakeElement("operator");
+        stubWrapper([operator]);
+
+        component.showNumWorkers = true;
+        component.toggleStatus();
+
+        
expect(operator.attr).toHaveBeenCalledWith(".texera-operator-state/ref-y", -55);
+      });
+    });
+  });
 });
diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts 
b/frontend/src/app/workspace/component/menu/menu.component.ts
index 6375fbcee4..b5621d06d3 100644
--- a/frontend/src/app/workspace/component/menu/menu.component.ts
+++ b/frontend/src/app/workspace/component/menu/menu.component.ts
@@ -321,8 +321,8 @@ export class MenuComponent implements OnInit, OnDestroy {
     const refY = this.showNumWorkers ? -55 : -35;
     const paperModel = 
this.workflowActionService.getJointGraphWrapper().mainPaper.model as any;
     paperModel.getElements().forEach((el: any) => {
-      el.attr(".operator-status/ref-x", -10);
-      el.attr(".operator-status/ref-y", refY);
+      el.attr(".texera-operator-state/ref-x", -10);
+      el.attr(".texera-operator-state/ref-y", refY);
     });
   }
 
@@ -542,11 +542,9 @@ export class MenuComponent implements OnInit, OnDestroy {
   }
 
   public toggleRegion(): void {
-    this.workflowActionService
-      .getJointGraphWrapper()
-      .jointGraph.getElements()
-      .filter(el => el.get("type") === "region") // small improvement here too
-      .forEach(el => el.attr("body/visibility", this.showRegion ? "visible" : 
"hidden"));
+    // The editor owns applying this to the shared JointJS model (both canvas 
and mini-map) and
+    // reapplies it whenever regions are recreated during execution (see 
#5120, #4027).
+    
this.workflowActionService.getJointGraphWrapper().setRegionsDisplayed(this.showRegion);
   }
 
   /**
diff --git 
a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss
 
b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss
index 422f743b71..482dac56a2 100644
--- 
a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss
+++ 
b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss
@@ -25,10 +25,6 @@
   height: 100%;
 }
 
-::ng-deep .hide-region .region {
-  display: none;
-}
-
 ::ng-deep .agent-action {
   // Agent action highlights - temporary 5-second visual indicators
   // Styles are defined inline in JointJS element, but this provides a hook 
for customization
@@ -39,6 +35,10 @@
   display: none;
 }
 
+::ng-deep .hide-operator-status .texera-operator-state {
+  display: none;
+}
+
 // Inline panels overlay container
 .panels-container {
   position: absolute;
diff --git 
a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts
 
b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts
index 38c8c8b113..a3cb50774c 100644
--- 
a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts
@@ -117,6 +117,64 @@ describe("WorkflowEditorComponent", () => {
       expect(component).toBeTruthy();
     });
 
+    it("should hide operator status on the canvas by default", () => {
+      // keeps the Status toggle off until the user enables it
+      const editor = (component as any).editor as HTMLElement;
+      expect(editor.classList.contains("hide-operator-status")).toBe(true);
+    });
+
+    // Drives the region-update stream the editor subscribes to in 
handleRegionEvents, creating
+    // region-<id> elements around the given operator, and returns the 
operator id used.
+    function emitRegionUpdate(regionId: number): string {
+      const operatorID = `region_op_${regionId}`;
+      const operator = new joint.shapes.basic.Rect({ position: { x: 0, y: 0 }, 
size: { width: 80, height: 40 } });
+      operator.set("id", operatorID);
+      jointGraph.addCell(operator);
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      (executeWorkflowService as any).regionUpdateStream.next({ regions: 
[[regionId, [operatorID]]] });
+      return operatorID;
+    }
+
+    it("should create region elements hidden so the Regions toggle starts off 
on canvas and mini-map", () => {
+      emitRegionUpdate(1);
+
+      const region = jointGraph.getCell("region-1");
+      expect(region).toBeTruthy();
+      // region visibility is a shared-model attribute, so hidden-by-default 
applies to both surfaces
+      expect(region.attr("body/visibility")).toBe("hidden");
+    });
+
+    it("should show regions created during execution when the toggle is 
already on", () => {
+      // user enables Regions, then execution emits region updates
+      const wrapper = 
TestBed.inject(WorkflowActionService).getJointGraphWrapper();
+      wrapper.setRegionsDisplayed(true);
+      emitRegionUpdate(1);
+
+      
expect(jointGraph.getCell("region-1").attr("body/visibility")).toBe("visible");
+    });
+
+    it("should keep regions visible when they are recreated on a later 
execution update", () => {
+      const wrapper = 
TestBed.inject(WorkflowActionService).getJointGraphWrapper();
+      wrapper.setRegionsDisplayed(true);
+      emitRegionUpdate(1);
+      // a subsequent update removes and recreates the region elements
+      emitRegionUpdate(2);
+
+      
expect(jointGraph.getCell("region-2").attr("body/visibility")).toBe("visible");
+    });
+
+    it("should toggle visibility of existing regions when the displayed flag 
changes", () => {
+      const wrapper = 
TestBed.inject(WorkflowActionService).getJointGraphWrapper();
+      emitRegionUpdate(1);
+      
expect(jointGraph.getCell("region-1").attr("body/visibility")).toBe("hidden");
+
+      wrapper.setRegionsDisplayed(true);
+      
expect(jointGraph.getCell("region-1").attr("body/visibility")).toBe("visible");
+
+      wrapper.setRegionsDisplayed(false);
+      
expect(jointGraph.getCell("region-1").attr("body/visibility")).toBe("hidden");
+    });
+
     it("should create element in the UI after adding operator in the model", 
() => {
       const operatorID = "test_one_operator_1";
 
diff --git 
a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
 
b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
index 979f131ad3..54e9ec9a4e 100644
--- 
a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
+++ 
b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts
@@ -275,6 +275,7 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       height: this.editor.offsetHeight,
     });
     this.editor.classList.add("hide-worker-count");
+    this.editor.classList.add("hide-operator-status");
   }
 
   private handleDisableJointPaperInteractiveness(): void {
@@ -361,7 +362,6 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
   }
 
   private handleRegionEvents(): void {
-    this.editor.classList.add("hide-region");
     const Region = joint.dia.Element.define(
       "region",
       {
@@ -369,7 +369,9 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
           body: {
             fill: "rgba(158,158,158,0.2)",
             pointerEvents: "none",
-            class: "region",
+            // Regions start hidden and are revealed via the View > Regions 
toggle. Driving visibility
+            // through this model attribute keeps the main canvas and the 
mini-map in sync (see #4027).
+            visibility: "hidden",
           },
         },
       },
@@ -396,8 +398,16 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
           this.updateRegionElement(element, ops);
           return { regionElement: element, operators: ops };
         });
+        // regions are recreated on every update, so reapply the current 
toggle state to the new elements
+        this.setRegionsVisibility(this.wrapper.getRegionsDisplayed());
       });
 
+    // apply the View > Regions toggle to all existing region elements (canvas 
and mini-map share the model)
+    this.wrapper
+      .getRegionsDisplayedStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(displayed => this.setRegionsVisibility(displayed));
+
     this.paper.model.on("change:position", operator => {
       regionMap
         .filter(region => region.operators.includes(operator))
@@ -418,6 +428,13 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  private setRegionsVisibility(displayed: boolean): void {
+    this.paper.model
+      .getElements()
+      .filter(element => element.get("type") === "region")
+      .forEach(element => element.attr("body/visibility", displayed ? 
"visible" : "hidden"));
+  }
+
   private updateRegionElement(regionElement: joint.dia.Element, operators: 
joint.dia.Cell[]) {
     const points = operators.flatMap(op => {
       const { x, y, width, height } = op.getBBox(),
diff --git 
a/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.spec.ts
 
b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.spec.ts
index 495208f5dc..c212722314 100644
--- 
a/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.spec.ts
+++ 
b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.spec.ts
@@ -793,4 +793,29 @@ describe("JointGraphWrapperService", () => {
       })
     );
   });
+
+  describe("regions displayed flag", () => {
+    it("defaults to not displayed", () => {
+      expect(jointGraphWrapper.getRegionsDisplayed()).toBe(false);
+    });
+
+    it("updates the value when set", () => {
+      jointGraphWrapper.setRegionsDisplayed(true);
+      expect(jointGraphWrapper.getRegionsDisplayed()).toBe(true);
+
+      jointGraphWrapper.setRegionsDisplayed(false);
+      expect(jointGraphWrapper.getRegionsDisplayed()).toBe(false);
+    });
+
+    it("emits the current value to new subscribers and on every change", () => 
{
+      const emitted: boolean[] = [];
+      jointGraphWrapper.getRegionsDisplayedStream().subscribe(displayed => 
emitted.push(displayed));
+
+      jointGraphWrapper.setRegionsDisplayed(true);
+      jointGraphWrapper.setRegionsDisplayed(false);
+
+      // BehaviorSubject replays the initial false, then each subsequent change
+      expect(emitted).toEqual([false, true, false]);
+    });
+  });
 });
diff --git 
a/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts
 
b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts
index f47aa8ab87..b7a2bf05b5 100644
--- 
a/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts
+++ 
b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { fromEvent, Observable, ReplaySubject, Subject } from "rxjs";
+import { BehaviorSubject, fromEvent, Observable, ReplaySubject, Subject } from 
"rxjs";
 import { filter, map } from "rxjs/operators";
 import { LogicalPort, Point } from "../../../types/workflow-common.interface";
 import * as joint from "jointjs";
@@ -103,6 +103,11 @@ export class JointGraphWrapper {
 
   private mainJointPaperAttachedStream: Subject<joint.dia.Paper> = new 
ReplaySubject(1);
 
+  // Whether region hulls are shown on the canvas (View > Regions toggle). 
Kept here so that it
+  // survives the region elements being recreated on every execution update, 
and so the editor can
+  // reapply it to the shared model (covering both the main canvas and the 
mini-map).
+  private regionsDisplayedStream = new BehaviorSubject<boolean>(false);
+
   private elementPositions: Map<string, PositionInfo> = new Map<string, 
PositionInfo>();
   private listenPositionChange: boolean = true;
 
@@ -209,6 +214,21 @@ export class JointGraphWrapper {
     return this.mainJointPaperAttachedStream;
   }
 
+  /**
+   * Sets whether region hulls should be displayed on the canvas (and 
mini-map).
+   */
+  public setRegionsDisplayed(displayed: boolean): void {
+    this.regionsDisplayedStream.next(displayed);
+  }
+
+  public getRegionsDisplayed(): boolean {
+    return this.regionsDisplayedStream.value;
+  }
+
+  public getRegionsDisplayedStream(): Observable<boolean> {
+    return this.regionsDisplayedStream.asObservable();
+  }
+
   /**
    * This method is used to toggle the multiselect mode.
    * @param multiSelect

Reply via email to