From 466fb7f532362acb2a417dc4e3e5dfaff6134790 Mon Sep 17 00:00:00 2001 From: Bob Bai Date: Fri, 22 May 2026 16:32:09 -0700 Subject: [PATCH 1/5] fix(frontend): restore Regions and Status canvas toggles Both toggles broke after #4495 changed visibility from JointJS attributes to CSS classes. - Regions: drop the dead body/visibility loop; toggle the hide-region class only. - Status: re-add the hide-operator-status SCSS rule (retargeted to the renamed .texera-operator-state element), add the default class so Status starts hidden, and fix the stale selector in applyOperatorStatusPosition. --- .../src/app/workspace/component/menu/menu.component.ts | 10 +++------- .../workflow-editor/workflow-editor.component.scss | 4 ++++ .../workflow-editor/workflow-editor.component.ts | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index 6375fbcee4e..f27e85cbd81 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,7 @@ 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")); + this.workflowActionService.getJointGraphWrapper().mainPaper.el.classList.toggle("hide-region", !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 422f743b717..f9275df00ab 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 @@ -39,6 +39,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.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 979f131ad3c..d90a93eb36b 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 { From af3a6edaebb84107753ab37b8a984809e89b6052 Mon Sep 17 00:00:00 2001 From: Bob Bai Date: Sat, 30 May 2026 09:53:29 -0700 Subject: [PATCH 2/5] fix(frontend): keep mini-map region visibility and add toggle tests Restore the per-element body/visibility flip in toggleRegion that the previous cleanup removed; the mini-map shares the model but has no hide-region class, so it relies on this attribute (regression of #4027). Both the CSS class (main canvas, #5120) and the attribute (mini-map, #4027) are needed, now documented inline. Add unit tests for toggleRegion, toggleStatus, the status-label repositioning, and the default hide-region / hide-operator-status canvas classes. --- .../component/menu/menu.component.spec.ts | 91 +++++++++++++++++++ .../component/menu/menu.component.ts | 10 +- .../workflow-editor.component.spec.ts | 7 ++ 3 files changed, 107 insertions(+), 1 deletion(-) 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 76de1c1e1b2..d36635c67ff 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,95 @@ 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, + get(key: string) { + return key === "type" ? this.type : undefined; + }, + attr: vi.fn(function (this: { attrs: Record }, 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[]) { + 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("removes hide-region and shows region elements when enabled", () => { + const region = fakeElement("region"); + const operator = fakeElement("operator"); + const el = stubWrapper([region, operator]); + el.classList.add("hide-region"); + + component.showRegion = true; + component.toggleRegion(); + + // main canvas: class drives display, mini-map: per-element visibility attribute + expect(el.classList.contains("hide-region")).toBe(false); + expect(region.attr).toHaveBeenCalledWith("body/visibility", "visible"); + expect(operator.attr).not.toHaveBeenCalled(); + }); + + it("adds hide-region and hides region elements when disabled", () => { + const region = fakeElement("region"); + const el = stubWrapper([region]); + + component.showRegion = false; + component.toggleRegion(); + + expect(el.classList.contains("hide-region")).toBe(true); + expect(region.attr).toHaveBeenCalledWith("body/visibility", "hidden"); + }); + }); + + 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 f27e85cbd81..05a16a3d48a 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -542,7 +542,15 @@ export class MenuComponent implements OnInit, OnDestroy { } public toggleRegion(): void { - this.workflowActionService.getJointGraphWrapper().mainPaper.el.classList.toggle("hide-region", !this.showRegion); + const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper(); + // The main canvas hides regions via the `hide-region` CSS class (display: none on every `.region`). + jointGraphWrapper.mainPaper.el.classList.toggle("hide-region", !this.showRegion); + // The mini-map shares the same model but carries no `hide-region` class, so it relies on the + // per-element visibility attribute. Both mechanisms are required (see #5120 and #4027). + jointGraphWrapper.jointGraph + .getElements() + .filter(el => el.get("type") === "region") + .forEach(el => el.attr("body/visibility", this.showRegion ? "visible" : "hidden")); } /** 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 38c8c8b1138..e2bbc6d7d25 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,13 @@ describe("WorkflowEditorComponent", () => { expect(component).toBeTruthy(); }); + it("should hide regions and operator status on the canvas by default", () => { + // these classes keep the Regions/Status toggles off until the user enables them (see #5120) + const editor = (component as any).editor as HTMLElement; + expect(editor.classList.contains("hide-region")).toBe(true); + expect(editor.classList.contains("hide-operator-status")).toBe(true); + }); + it("should create element in the UI after adding operator in the model", () => { const operatorID = "test_one_operator_1"; From 61ecab52b78cff92febff2e7627fceffc24c1d61 Mon Sep 17 00:00:00 2001 From: Bob Bai Date: Sat, 30 May 2026 10:35:53 -0700 Subject: [PATCH 3/5] fix(frontend): drive region visibility via shared model attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch regions back to the pure body/visibility approach from #4112. #4495 had regions hidden via a hide-region CSS class on the main paper only, with the element defaulting to visible — so the mini-map (which shares the model but not the class) showed region hulls regardless of the toggle, and freshly-created regions reappeared on every execution update. Now the region element defaults to visibility: hidden and toggleRegion flips body/visibility on the shared model, so both the main canvas and the mini-map stay in sync (#5120 and #4027). Drops the hide-region class and SCSS rule. Tests updated accordingly. --- .../component/menu/menu.component.spec.ts | 13 +++++------- .../component/menu/menu.component.ts | 12 +++++------ .../workflow-editor.component.scss | 4 ---- .../workflow-editor.component.spec.ts | 21 ++++++++++++++++--- .../workflow-editor.component.ts | 5 +++-- 5 files changed, 31 insertions(+), 24 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 d36635c67ff..2807e1469ef 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts @@ -554,29 +554,26 @@ describe("MenuComponent", () => { } describe("toggleRegion", () => { - it("removes hide-region and shows region elements when enabled", () => { + it("shows only region elements when enabled", () => { const region = fakeElement("region"); const operator = fakeElement("operator"); - const el = stubWrapper([region, operator]); - el.classList.add("hide-region"); + stubWrapper([region, operator]); component.showRegion = true; component.toggleRegion(); - // main canvas: class drives display, mini-map: per-element visibility attribute - expect(el.classList.contains("hide-region")).toBe(false); + // visibility is a shared-model attribute, so the toggle reaches both the canvas and the mini-map expect(region.attr).toHaveBeenCalledWith("body/visibility", "visible"); expect(operator.attr).not.toHaveBeenCalled(); }); - it("adds hide-region and hides region elements when disabled", () => { + it("hides region elements when disabled", () => { const region = fakeElement("region"); - const el = stubWrapper([region]); + stubWrapper([region]); component.showRegion = false; component.toggleRegion(); - expect(el.classList.contains("hide-region")).toBe(true); expect(region.attr).toHaveBeenCalledWith("body/visibility", "hidden"); }); }); diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index 05a16a3d48a..f6065167348 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -542,13 +542,11 @@ export class MenuComponent implements OnInit, OnDestroy { } public toggleRegion(): void { - const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper(); - // The main canvas hides regions via the `hide-region` CSS class (display: none on every `.region`). - jointGraphWrapper.mainPaper.el.classList.toggle("hide-region", !this.showRegion); - // The mini-map shares the same model but carries no `hide-region` class, so it relies on the - // per-element visibility attribute. Both mechanisms are required (see #5120 and #4027). - jointGraphWrapper.jointGraph - .getElements() + // Region visibility is driven by the per-element `body/visibility` attribute on the shared + // JointJS model, so the toggle applies to both the main canvas and the mini-map (see #5120, #4027). + this.workflowActionService + .getJointGraphWrapper() + .jointGraph.getElements() .filter(el => el.get("type") === "region") .forEach(el => el.attr("body/visibility", this.showRegion ? "visible" : "hidden")); } 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 f9275df00ab..482dac56a26 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 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 e2bbc6d7d25..fa8a84775bd 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,13 +117,28 @@ describe("WorkflowEditorComponent", () => { expect(component).toBeTruthy(); }); - it("should hide regions and operator status on the canvas by default", () => { - // these classes keep the Regions/Status toggles off until the user enables them (see #5120) + 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-region")).toBe(true); expect(editor.classList.contains("hide-operator-status")).toBe(true); }); + it("should create region elements hidden so the Regions toggle starts off on canvas and mini-map", () => { + const operatorID = "region_default_op"; + const operator = new joint.shapes.basic.Rect({ position: { x: 0, y: 0 }, size: { width: 80, height: 40 } }); + operator.set("id", operatorID); + jointGraph.addCell(operator); + + // drive the region-update stream the editor subscribes to in handleRegionEvents + const executeWorkflowService = TestBed.inject(ExecuteWorkflowService); + (executeWorkflowService as any).regionUpdateStream.next({ regions: [[1, [operatorID]]] }); + + 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 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 d90a93eb36b..f6145e81937 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 @@ -362,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", { @@ -370,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", }, }, }, From b0cae9d89d97b707ed935749d74131fcd53d1baf Mon Sep 17 00:00:00 2001 From: Bob Bai Date: Sat, 30 May 2026 10:50:54 -0700 Subject: [PATCH 4/5] fix(frontend): persist region visibility across execution updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regions are recreated on every RegionUpdateEvent during execution, so a toggle that only flipped existing elements was lost as soon as the next update arrived — regions stayed hidden mid-run even with the toggle on. Make the Regions toggle a shared, persistent flag on JointGraphWrapper. The editor reapplies it to the shared JointJS model both when the flag changes and whenever regions are recreated, so the main canvas and the mini-map stay in sync on and off during execution (#5120, #4027). The menu toggle now just sets the flag; the editor owns applying it. Tests cover creation-while-on, recreation, and toggling existing regions. --- .../component/menu/menu.component.spec.ts | 17 +++---- .../component/menu/menu.component.ts | 10 ++-- .../workflow-editor.component.spec.ts | 46 +++++++++++++++++-- .../workflow-editor.component.ts | 15 ++++++ .../model/joint-graph-wrapper.ts | 22 ++++++++- 5 files changed, 86 insertions(+), 24 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 2807e1469ef..14491742159 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts @@ -554,27 +554,22 @@ describe("MenuComponent", () => { } describe("toggleRegion", () => { - it("shows only region elements when enabled", () => { - const region = fakeElement("region"); - const operator = fakeElement("operator"); - stubWrapper([region, operator]); + it("publishes the displayed flag to the joint graph wrapper when enabled", () => { + const setSpy = vi.spyOn(workflowActionService.getJointGraphWrapper(), "setRegionsDisplayed"); component.showRegion = true; component.toggleRegion(); - // visibility is a shared-model attribute, so the toggle reaches both the canvas and the mini-map - expect(region.attr).toHaveBeenCalledWith("body/visibility", "visible"); - expect(operator.attr).not.toHaveBeenCalled(); + expect(setSpy).toHaveBeenCalledWith(true); }); - it("hides region elements when disabled", () => { - const region = fakeElement("region"); - stubWrapper([region]); + 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(region.attr).toHaveBeenCalledWith("body/visibility", "hidden"); + expect(setSpy).toHaveBeenCalledWith(false); }); }); diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index f6065167348..b5621d06d3a 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -542,13 +542,9 @@ export class MenuComponent implements OnInit, OnDestroy { } public toggleRegion(): void { - // Region visibility is driven by the per-element `body/visibility` attribute on the shared - // JointJS model, so the toggle applies to both the main canvas and the mini-map (see #5120, #4027). - this.workflowActionService - .getJointGraphWrapper() - .jointGraph.getElements() - .filter(el => el.get("type") === "region") - .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.spec.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts index fa8a84775bd..a3cb50774c2 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 @@ -123,15 +123,20 @@ describe("WorkflowEditorComponent", () => { expect(editor.classList.contains("hide-operator-status")).toBe(true); }); - it("should create region elements hidden so the Regions toggle starts off on canvas and mini-map", () => { - const operatorID = "region_default_op"; + // Drives the region-update stream the editor subscribes to in handleRegionEvents, creating + // region- 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); - - // drive the region-update stream the editor subscribes to in handleRegionEvents const executeWorkflowService = TestBed.inject(ExecuteWorkflowService); - (executeWorkflowService as any).regionUpdateStream.next({ regions: [[1, [operatorID]]] }); + (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(); @@ -139,6 +144,37 @@ describe("WorkflowEditorComponent", () => { 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 f6145e81937..54e9ec9a4e8 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 @@ -398,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)) @@ -420,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.ts b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts index f47aa8ab87d..b7a2bf05b51 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 = 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(false); + private elementPositions: Map = new Map(); 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 { + return this.regionsDisplayedStream.asObservable(); + } + /** * This method is used to toggle the multiselect mode. * @param multiSelect From a42d6fc669b3b7db2e26bd0c7e497e3ccffec354 Mon Sep 17 00:00:00 2001 From: Bob Bai Date: Sat, 30 May 2026 13:08:37 -0700 Subject: [PATCH 5/5] test(frontend): cover JointGraphWrapper region-displayed flag Direct tests for setRegionsDisplayed/getRegionsDisplayed and the BehaviorSubject stream (default, update, replay-and-change). --- .../model/joint-graph-wrapper.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 495208f5dce..c212722314d 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]); + }); + }); });