This is an automated email from the ASF dual-hosted git repository.
Yicong-Huang 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 20ef8254f2 refactor(ui): encapsulate OperatorMenuService state and add
subscription cleanup (#4530)
20ef8254f2 is described below
commit 20ef8254f209f36d1fb8ee519756c046a53b5475
Author: Yicong Huang <[email protected]>
AuthorDate: Thu Apr 30 22:48:29 2026 -0700
refactor(ui): encapsulate OperatorMenuService state and add subscription
cleanup (#4530)
### What changes were proposed in this PR?
Refactor
`frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts`
to address two coupled code-quality issues:
1. **Encapsulate state.** `highlightedOperators` and
`highlightedCommentBoxes` are now private `BehaviorSubject`s, exposed
only as readonly `Observable`s (`highlightedOperators$`,
`highlightedCommentBoxes$`). External callers can no longer call
`.next()` and corrupt service state.
2. **Add subscription cleanup.** All five constructor subscriptions now
use `untilDestroyed(this)` (consistent with the rest of the codebase).
HMR no longer leaves ghost subscriptions on the old `JointGraphWrapper`
/ `TexeraGraph` instances.
3. **Eliminate fan-out.** Previously, three downstream handlers
(`handleDisableOperatorStatusChange`,
`handleViewResultOperatorStatusChange`,
`handleReuseOperatorResultStatusChange`) all subscribed to the public
`highlightedOperators` BehaviorSubject. A single user highlight action
triggered the upstream handler that called `.next()`, which then
re-fired all three downstream handlers — a 4× cascade per highlight
event. The refactored code uses one private `recomputeMenuState()`
method invoked directly from the upstream handler, plus a single
`merge(...)` over the texera-graph state-change streams. One highlight =
one recompute.
### Consumer migration
- `ContextMenuComponent`: subscribes to the two Observables in its
constructor (with `untilDestroyed`) and stores `highlightedOperatorIds`
/ `highlightedCommentBoxIds` as local fields for the template.
- `context-menu.component.html`: template `*ngIf` checks now reference
the component's local fields instead of
`operatorMenuService.highlightedOperators.value`.
- `WorkflowEditorComponent` copy/cut handlers: use
`withLatestFrom(operatorMenu.highlightedOperators$,
operatorMenu.highlightedCommentBoxes$)` instead of reading `.value`.
### Any related issues, documentation, discussions?
Closes #4529
### How was this PR tested?
- Replaced the placeholder `OperatorMenuService` spec with 9 new tests
covering: empty initial state, encapsulation (BehaviorSubjects no longer
publicly accessible), highlight/unhighlight propagation to both
observables, the no-fan-out invariant (a single highlight produces
exactly one emission on `highlightedOperators$`), sink exclusion in
view-result/reuse-result targets, and recomputation triggered by
`getViewResultOperatorsChangedStream` and the
workflow-modification-enabled stream.
- Existing `ContextMenuComponent`, `WorkflowEditorComponent`,
`PropertyEditorComponent`, `ResultPanelComponent`, and
`JointGraphWrapper` specs continue to pass.
- `yarn ng build` is clean (no new TS errors).
- `yarn format:fix` reports no changes.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (claude-opus-4-7)
---
.../context-menu/context-menu.component.html | 24 +--
.../context-menu/context-menu.component.spec.ts | 6 +-
.../context-menu/context-menu.component.ts | 8 +
.../workflow-editor/workflow-editor.component.ts | 18 +--
.../operator-menu/operator-menu.service.spec.ts | 139 ++++++++++++++++++
.../service/operator-menu/operator-menu.service.ts | 162 +++++++++------------
6 files changed, 238 insertions(+), 119 deletions(-)
diff --git
a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html
b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html
index 7a6fcba6f2..4465d65cb2 100644
---
a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html
+++
b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html
@@ -20,8 +20,8 @@
<ul nz-menu>
<li
nz-menu-item
- *ngIf="(operatorMenuService.highlightedOperators.value.length > 0 ||
- operatorMenuService.highlightedCommentBoxes.value.length > 0) &&
+ *ngIf="(highlightedOperatorIds.length > 0 ||
+ highlightedCommentBoxIds.length > 0) &&
!hasHighlightedLinks()"
(click)="onCopy()">
<span
@@ -32,8 +32,8 @@
</li>
<li
nz-menu-item
- *ngIf="(operatorMenuService.highlightedOperators.value.length > 0 ||
- operatorMenuService.highlightedCommentBoxes.value.length > 0) &&
+ *ngIf="(highlightedOperatorIds.length > 0 ||
+ highlightedCommentBoxIds.length > 0) &&
!hasHighlightedLinks() &&
isWorkflowModifiable"
(click)="onCut()">
@@ -45,8 +45,8 @@
</li>
<li
nz-menu-item
- *ngIf="(operatorMenuService.highlightedOperators.value.length === 0 &&
- operatorMenuService.highlightedCommentBoxes.value.length === 0 &&
+ *ngIf="(highlightedOperatorIds.length === 0 &&
+ highlightedCommentBoxIds.length === 0 &&
!hasHighlightedLinks()) &&
isWorkflowModifiable"
(click)="onPaste()">
@@ -125,8 +125,8 @@
</li>
<li
nz-menu-item
- *ngIf="(operatorMenuService.highlightedOperators.value.length > 0 ||
- operatorMenuService.highlightedCommentBoxes.value.length > 0) &&
+ *ngIf="(highlightedOperatorIds.length > 0 ||
+ highlightedCommentBoxIds.length > 0) &&
isWorkflowModifiable"
(click)="onDelete()">
<span
@@ -140,8 +140,8 @@
<li
nz-menu-item
*ngIf="hasHighlightedLinks() &&
- operatorMenuService.highlightedOperators.value.length === 0 &&
- operatorMenuService.highlightedCommentBoxes.value.length === 0 &&
+ highlightedOperatorIds.length === 0 &&
+ highlightedCommentBoxIds.length === 0 &&
isWorkflowModifiable"
(click)="onDelete()">
<span
@@ -153,8 +153,8 @@
<li
nz-menu-item
- *ngIf="operatorMenuService.highlightedOperators.value.length === 1 &&
- operatorMenuService.highlightedCommentBoxes.value.length === 0 &&
+ *ngIf="highlightedOperatorIds.length === 1 &&
+ highlightedCommentBoxIds.length === 0 &&
!hasHighlightedLinks()"
[nzDisabled]="!canExecuteOperator()"
(click)="operatorMenuService.executeUpToOperator()">
diff --git
a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
index 115240c4e0..f7e31f40b3 100644
---
a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
+++
b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
@@ -27,7 +27,7 @@ import { WorkflowActionService } from
"src/app/workspace/service/workflow-graph/
import { WorkflowResultService } from
"src/app/workspace/service/workflow-result/workflow-result.service";
import { WorkflowResultExportService } from
"src/app/workspace/service/workflow-result-export/workflow-result-export.service";
import { OperatorMenuService } from
"src/app/workspace/service/operator-menu/operator-menu.service";
-import { BehaviorSubject, of } from "rxjs";
+import { of } from "rxjs";
import { ReactiveFormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { NzDropDownModule } from "ng-zorro-antd/dropdown";
@@ -94,8 +94,8 @@ describe("ContextMenuComponent", () => {
// Create a mock for OperatorMenuService with necessary properties and
methods
operatorMenuService = {
- highlightedOperators: new BehaviorSubject<any[]>([]),
- highlightedCommentBoxes: new BehaviorSubject<any[]>([]),
+ highlightedOperators$: of([] as readonly string[]),
+ highlightedCommentBoxes$: of([] as readonly string[]),
isDisableOperator: false,
isDisableOperatorClickable: false,
isToViewResult: false,
diff --git
a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.ts
b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.ts
index 15d3917a06..2dd0b67dee 100644
---
a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.ts
+++
b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.ts
@@ -37,6 +37,8 @@ import { GuiConfigService } from
"../../../../../common/service/gui-config.servi
})
export class ContextMenuComponent {
public isWorkflowModifiable: boolean = false;
+ public highlightedOperatorIds: readonly string[] = [];
+ public highlightedCommentBoxIds: readonly string[] = [];
constructor(
public workflowActionService: WorkflowActionService,
@@ -48,6 +50,12 @@ export class ContextMenuComponent {
private validationWorkflowService: ValidationWorkflowService
) {
this.registerWorkflowModifiableChangedHandler();
+ this.operatorMenuService.highlightedOperators$
+ .pipe(untilDestroyed(this))
+ .subscribe(ids => (this.highlightedOperatorIds = ids));
+ this.operatorMenuService.highlightedCommentBoxes$
+ .pipe(untilDestroyed(this))
+ .subscribe(ids => (this.highlightedCommentBoxIds = ids));
}
public canExecuteOperator(): boolean {
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 6a0fcf4b1d..fc6deeab85 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
@@ -30,7 +30,7 @@ import { WorkflowActionService } from
"../../service/workflow-graph/model/workfl
import { WorkflowStatusService } from
"../../service/workflow-status/workflow-status.service";
import { ExecutionState, OperatorState } from
"../../types/execute-workflow.interface";
import { LogicalPort, OperatorLink, OperatorPredicate } from
"../../types/workflow-common.interface";
-import { auditTime, filter, map, takeUntil } from "rxjs/operators";
+import { auditTime, filter, map, takeUntil, withLatestFrom } from
"rxjs/operators";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { UndoRedoService } from "../../service/undo-redo/undo-redo.service";
import { WorkflowVersionService } from
"../../../dashboard/service/user/workflow-version/workflow-version.service";
@@ -1126,13 +1126,11 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
fromEvent<ClipboardEvent>(document, "copy")
.pipe(
filter(_ => document.activeElement === document.body),
+ withLatestFrom(this.operatorMenu.highlightedOperators$,
this.operatorMenu.highlightedCommentBoxes$),
untilDestroyed(this)
)
- .subscribe(() => {
- if (
- this.operatorMenu.highlightedOperators.value.length > 0 ||
- this.operatorMenu.highlightedCommentBoxes.value.length > 0
- ) {
+ .subscribe(([_, highlightedOperators, highlightedCommentBoxes]) => {
+ if (highlightedOperators.length > 0 || highlightedCommentBoxes.length
> 0) {
this.operatorMenu.saveHighlightedElements();
}
});
@@ -1148,13 +1146,11 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
.pipe(
filter(() => document.activeElement === document.body),
filter(() => this.interactive),
+ withLatestFrom(this.operatorMenu.highlightedOperators$,
this.operatorMenu.highlightedCommentBoxes$),
untilDestroyed(this)
)
- .subscribe(() => {
- if (
- this.operatorMenu.highlightedOperators.value.length > 0 ||
- this.operatorMenu.highlightedCommentBoxes.value.length > 0
- ) {
+ .subscribe(([_, highlightedOperators, highlightedCommentBoxes]) => {
+ if (highlightedOperators.length > 0 || highlightedCommentBoxes.length
> 0) {
this.operatorMenu.saveHighlightedElements();
this.deleteElements();
}
diff --git
a/frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts
b/frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts
index 8b4f3c9b5a..2e20f52ac0 100644
---
a/frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts
+++
b/frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts
@@ -26,9 +26,22 @@ import { HttpClientModule } from "@angular/common/http";
import { ComputingUnitStatusService } from
"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
import { MockComputingUnitStatusService } from
"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service";
import { commonTestProviders } from "../../../common/testing/test-utils";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import {
+ mockCommentBox,
+ mockPoint,
+ mockResultPredicate,
+ mockScanPredicate,
+ mockSentimentPredicate,
+} from "../workflow-graph/model/mock-workflow-data";
+import { Subscription } from "rxjs";
describe("OperatorMenuService", () => {
let service: OperatorMenuService;
+ let workflowActionService: WorkflowActionService;
+ let opsLatest: readonly string[] = [];
+ let boxesLatest: readonly string[] = [];
+ let subs: Subscription;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -39,10 +52,136 @@ describe("OperatorMenuService", () => {
],
imports: [HttpClientModule],
});
+ workflowActionService = TestBed.inject(WorkflowActionService);
service = TestBed.inject(OperatorMenuService);
+
+ subs = new Subscription();
+ subs.add(service.highlightedOperators$.subscribe(ids => (opsLatest =
ids)));
+ subs.add(service.highlightedCommentBoxes$.subscribe(ids => (boxesLatest =
ids)));
});
+ afterEach(() => subs.unsubscribe());
+
it("should be created", () => {
expect(service).toBeTruthy();
});
+
+ it("starts with empty highlighted snapshots", () => {
+ expect(opsLatest).toEqual([]);
+ expect(boxesLatest).toEqual([]);
+ });
+
+ it("does not expose mutable BehaviorSubjects on the public API", () => {
+ // service must not let outside code call .next() on its internal state.
+ expect((service as any).highlightedOperators).toBeUndefined();
+ expect((service as any).highlightedCommentBoxes).toBeUndefined();
+ });
+
+ it("emits the new highlighted operator IDs on highlightedOperators$", () => {
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+
+ expect(opsLatest).toEqual([mockScanPredicate.operatorID]);
+ });
+
+ it("emits the new highlighted comment box IDs on highlightedCommentBoxes$",
() => {
+ workflowActionService.addCommentBox(mockCommentBox);
+
workflowActionService.getJointGraphWrapper().highlightCommentBoxes(mockCommentBox.commentBoxID);
+
+ expect(boxesLatest).toEqual([mockCommentBox.commentBoxID]);
+ });
+
+ it("emits exactly once on highlightedOperators$ per highlight change (no
fan-out)", () => {
+ const emissions: string[][] = [];
+ const sub = service.highlightedOperators$.subscribe(ids =>
emissions.push([...ids]));
+ // BehaviorSubject seed
+ expect(emissions.length).toBe(1);
+
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+
+ // a single highlight event must produce a single emission, not 4 (one per
dependent handler).
+ expect(emissions.length).toBe(2);
+ expect(emissions[1]).toEqual([mockScanPredicate.operatorID]);
+
+
workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID);
+ expect(emissions.length).toBe(3);
+ expect(emissions[2]).toEqual([]);
+
+ sub.unsubscribe();
+ });
+
+ describe("button state recomputation", () => {
+ it("makes disable button clickable when an operator is highlighted and
modification is enabled", () => {
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+
+ expect(service.isDisableOperatorClickable).toBeTrue();
+ expect(service.isDisableOperator).toBeTrue();
+ });
+
+ it("flips isDisableOperator to enable after the operator is disabled", ()
=> {
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+ workflowActionService.disableOperators([mockScanPredicate.operatorID]);
+
+ // all highlighted operators are now disabled, so clicking should
re-enable them.
+ expect(service.isDisableOperator).toBeFalse();
+ });
+
+ it("excludes sinks from view-result targets", () => {
+ workflowActionService.addOperatorsAndLinks(
+ [
+ { op: mockScanPredicate, pos: mockPoint },
+ { op: mockResultPredicate, pos: mockPoint },
+ ],
+ []
+ );
+ const wrapper = workflowActionService.getJointGraphWrapper();
+ // start from a clean highlight state — addOperator may auto-highlight
new operators.
+
wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs());
+
+ // highlighting only a sink: view-result should not be clickable.
+ wrapper.highlightOperators(mockResultPredicate.operatorID);
+ expect(service.isToViewResultClickable).toBeFalse();
+ expect(service.isReuseResultClickable).toBeFalse();
+
+ // highlighting only a non-sink: view-result becomes clickable.
+ wrapper.unhighlightOperators(mockResultPredicate.operatorID);
+ wrapper.highlightOperators(mockScanPredicate.operatorID);
+ expect(service.isToViewResultClickable).toBeTrue();
+ expect(service.isReuseResultClickable).toBeTrue();
+ });
+
+ it("recomputes when modification-enabled stream fires without a highlight
change", () => {
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+ expect(service.isDisableOperatorClickable).toBeTrue();
+
+ workflowActionService.disableWorkflowModification();
+ expect(service.isDisableOperatorClickable).toBeFalse();
+
+ workflowActionService.enableWorkflowModification();
+ expect(service.isDisableOperatorClickable).toBeTrue();
+ });
+
+ it("recomputes when view-result state of a highlighted non-sink operator
changes", () => {
+ workflowActionService.addOperatorsAndLinks(
+ [
+ { op: mockScanPredicate, pos: mockPoint },
+ { op: mockSentimentPredicate, pos: mockPoint },
+ ],
+ []
+ );
+ workflowActionService
+ .getJointGraphWrapper()
+ .highlightOperators(mockScanPredicate.operatorID,
mockSentimentPredicate.operatorID);
+
+ expect(service.isToViewResult).toBeTrue();
+
+
workflowActionService.setViewOperatorResults([mockScanPredicate.operatorID,
mockSentimentPredicate.operatorID]);
+ // both highlighted non-sinks are now viewing results → next click
should toggle off.
+ expect(service.isToViewResult).toBeFalse();
+ });
+ });
});
diff --git
a/frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts
b/frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts
index ac39301d24..1b800e9f7b 100644
--- a/frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts
+++ b/frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts
@@ -20,11 +20,12 @@
import { Injectable } from "@angular/core";
import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
import { isSink } from "../workflow-graph/model/workflow-graph";
-import { BehaviorSubject, merge } from "rxjs";
+import { BehaviorSubject, merge, Observable } from "rxjs";
import { CommentBox, OperatorLink, OperatorPredicate, Point } from
"../../types/workflow-common.interface";
import { WorkflowUtilService } from
"../workflow-graph/util/workflow-util.service";
import { NotificationService } from
"src/app/common/service/notification/notification.service";
import { ExecuteWorkflowService } from
"../execute-workflow/execute-workflow.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
type OperatorPositions = {
[key: string]: Point;
@@ -50,12 +51,17 @@ type SerializedString = {
* - right-click menu
* - keyboard shortcuts
*/
+@UntilDestroy()
@Injectable({
providedIn: "root",
})
export class OperatorMenuService {
- public highlightedOperators = new BehaviorSubject([] as readonly string[]);
- public highlightedCommentBoxes = new BehaviorSubject([] as readonly
string[]);
+ private readonly _highlightedOperators$ = new BehaviorSubject<readonly
string[]>([]);
+ private readonly _highlightedCommentBoxes$ = new BehaviorSubject<readonly
string[]>([]);
+
+ public readonly highlightedOperators$: Observable<readonly string[]> =
this._highlightedOperators$.asObservable();
+ public readonly highlightedCommentBoxes$: Observable<readonly string[]> =
+ this._highlightedCommentBoxes$.asObservable();
// whether the disable-operator-button should be enabled
public isDisableOperatorClickable: boolean = false;
@@ -75,29 +81,38 @@ export class OperatorMenuService {
private notificationService: NotificationService,
private executeWorkflowService: ExecuteWorkflowService
) {
- this.handleDisableOperatorStatusChange();
- this.handleViewResultOperatorStatusChange();
- this.handleReuseOperatorResultStatusChange();
+ const jointGraphWrapper =
this.workflowActionService.getJointGraphWrapper();
+ const texeraGraph = this.workflowActionService.getTexeraGraph();
merge(
-
this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(),
-
this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream(),
-
this.workflowActionService.getJointGraphWrapper().getJointGroupHighlightStream(),
-
this.workflowActionService.getJointGraphWrapper().getJointGroupUnhighlightStream()
- ).subscribe(() => {
- this.highlightedOperators.next(
-
this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()
- );
- });
+ jointGraphWrapper.getJointOperatorHighlightStream(),
+ jointGraphWrapper.getJointOperatorUnhighlightStream(),
+ jointGraphWrapper.getJointGroupHighlightStream(),
+ jointGraphWrapper.getJointGroupUnhighlightStream()
+ )
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+
this._highlightedOperators$.next(jointGraphWrapper.getCurrentHighlightedOperatorIDs());
+ this.recomputeMenuState();
+ });
merge(
-
this.workflowActionService.getJointGraphWrapper().getJointCommentBoxHighlightStream(),
-
this.workflowActionService.getJointGraphWrapper().getJointCommentBoxUnhighlightStream()
- ).subscribe(() => {
- this.highlightedCommentBoxes.next(
-
this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedCommentBoxIDs()
- );
- });
+ jointGraphWrapper.getJointCommentBoxHighlightStream(),
+ jointGraphWrapper.getJointCommentBoxUnhighlightStream()
+ )
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+
this._highlightedCommentBoxes$.next(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs());
+ });
+
+ merge(
+ texeraGraph.getDisabledOperatorsChangedStream(),
+ texeraGraph.getViewResultOperatorsChangedStream(),
+ texeraGraph.getReuseCacheOperatorsChangedStream(),
+ this.workflowActionService.getWorkflowModificationEnabledStream()
+ )
+ .pipe(untilDestroyed(this))
+ .subscribe(() => this.recomputeMenuState());
}
/**
@@ -105,98 +120,59 @@ export class OperatorMenuService {
* this.isDisableOperator indicates whether the operators should be disabled
or enabled
*/
public disableHighlightedOperators(): void {
+ const highlighted = this._highlightedOperators$.value;
if (this.isDisableOperator) {
-
this.workflowActionService.disableOperators(this.highlightedOperators.value);
+ this.workflowActionService.disableOperators(highlighted);
} else {
-
this.workflowActionService.enableOperators(this.highlightedOperators.value);
+ this.workflowActionService.enableOperators(highlighted);
}
}
public viewResultHighlightedOperators(): void {
- const effectiveHighlightedOperatorsExcludeSink =
this.highlightedOperators.value.filter(
- op =>
!isSink(this.workflowActionService.getTexeraGraph().getOperator(op))
- );
-
+ const targets = this.highlightedOperatorIdsExcludingSinks();
if (this.isToViewResult) {
-
this.workflowActionService.setViewOperatorResults(effectiveHighlightedOperatorsExcludeSink);
+ this.workflowActionService.setViewOperatorResults(targets);
} else {
-
this.workflowActionService.unsetViewOperatorResults(effectiveHighlightedOperatorsExcludeSink);
+ this.workflowActionService.unsetViewOperatorResults(targets);
}
}
public reuseResultHighlightedOperator(): void {
- const effectiveHighlightedOperatorsExcludeSink =
this.highlightedOperators.value.filter(
- op =>
!isSink(this.workflowActionService.getTexeraGraph().getOperator(op))
- );
-
+ const targets = this.highlightedOperatorIdsExcludingSinks();
if (this.isMarkForReuse) {
-
this.workflowActionService.markReuseResults(effectiveHighlightedOperatorsExcludeSink);
+ this.workflowActionService.markReuseResults(targets);
} else {
-
this.workflowActionService.removeMarkReuseResults(effectiveHighlightedOperatorsExcludeSink);
+ this.workflowActionService.removeMarkReuseResults(targets);
}
}
/**
- * Updates the status of the disable operator icon:
- * If all selected operators are disabled, then click it will re-enable the
operators
- * If any of the selected operator is not disabled, then click will disable
all selected operators
+ * Recomputes the three button states from current state. Called whenever
+ * highlighted operators change or any underlying texera-graph state changes
—
+ * a single linear update path that replaces the previous fan-out via shared
BehaviorSubject.
*/
- handleDisableOperatorStatusChange() {
- merge(
- this.highlightedOperators,
-
this.workflowActionService.getTexeraGraph().getDisabledOperatorsChangedStream(),
- this.workflowActionService.getWorkflowModificationEnabledStream()
- ).subscribe(event => {
- const allDisabled = this.highlightedOperators.value.every(op =>
- this.workflowActionService.getTexeraGraph().isOperatorDisabled(op)
- );
-
- this.isDisableOperator = !allDisabled;
- this.isDisableOperatorClickable =
- this.highlightedOperators.value.length !== 0 &&
this.workflowActionService.checkWorkflowModificationEnabled();
- });
- }
-
- handleViewResultOperatorStatusChange() {
- merge(
- this.highlightedOperators,
-
this.workflowActionService.getTexeraGraph().getViewResultOperatorsChangedStream(),
- this.workflowActionService.getWorkflowModificationEnabledStream()
- ).subscribe(event => {
- const effectiveHighlightedOperatorsExcludeSink =
this.highlightedOperators.value.filter(
- op =>
!isSink(this.workflowActionService.getTexeraGraph().getOperator(op))
- );
-
- const allViewing = effectiveHighlightedOperatorsExcludeSink.every(op =>
- this.workflowActionService.getTexeraGraph().isViewingResult(op)
- );
-
- this.isToViewResult = !allViewing;
- this.isToViewResultClickable =
- effectiveHighlightedOperatorsExcludeSink.length !== 0 &&
- this.workflowActionService.checkWorkflowModificationEnabled();
- });
+ private recomputeMenuState(): void {
+ const texeraGraph = this.workflowActionService.getTexeraGraph();
+ const modificationEnabled =
this.workflowActionService.checkWorkflowModificationEnabled();
+ const highlighted = this._highlightedOperators$.value;
+ const highlightedExcludingSinks =
this.highlightedOperatorIdsExcludingSinks();
+
+ const allDisabled = highlighted.every(op =>
texeraGraph.isOperatorDisabled(op));
+ this.isDisableOperator = !allDisabled;
+ this.isDisableOperatorClickable = highlighted.length !== 0 &&
modificationEnabled;
+
+ const allViewing = highlightedExcludingSinks.every(op =>
texeraGraph.isViewingResult(op));
+ this.isToViewResult = !allViewing;
+ this.isToViewResultClickable = highlightedExcludingSinks.length !== 0 &&
modificationEnabled;
+
+ const allMarkedForReuse = highlightedExcludingSinks.every(op =>
texeraGraph.isMarkedForReuseResult(op));
+ this.isMarkForReuse = !allMarkedForReuse;
+ this.isReuseResultClickable = highlightedExcludingSinks.length !== 0 &&
modificationEnabled;
}
- handleReuseOperatorResultStatusChange() {
- merge(
- this.highlightedOperators,
-
this.workflowActionService.getTexeraGraph().getReuseCacheOperatorsChangedStream(),
- this.workflowActionService.getWorkflowModificationEnabledStream()
- ).subscribe(event => {
- const effectiveHighlightedOperatorsExcludeSink =
this.highlightedOperators.value.filter(
- op =>
!isSink(this.workflowActionService.getTexeraGraph().getOperator(op))
- );
-
- const allMarkedForReuse =
effectiveHighlightedOperatorsExcludeSink.every(op =>
- this.workflowActionService.getTexeraGraph().isMarkedForReuseResult(op)
- );
-
- this.isMarkForReuse = !allMarkedForReuse;
- this.isReuseResultClickable =
- effectiveHighlightedOperatorsExcludeSink.length !== 0 &&
- this.workflowActionService.checkWorkflowModificationEnabled();
- });
+ private highlightedOperatorIdsExcludingSinks(): string[] {
+ const texeraGraph = this.workflowActionService.getTexeraGraph();
+ return this._highlightedOperators$.value.filter(op =>
!isSink(texeraGraph.getOperator(op)));
}
/**