Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions frontend/src/app/common/type/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,16 @@ export interface FormBindingConfig {
};
/** Array order is display order; the author reorders by dragging. */
fields: FormFieldBinding[];
/** View-result operators whose results are also shown under the workflow after a run, on top of
* the final step's result, which always shows. */
resultOperatorIds: string[];
/** Which steps' results show under the workflow after a run, for everyone. Absent until the author
* chooses: then every final (terminal) step shows, as on the canvas. Once set it is exhaustive:
* exactly these steps show, and [] means none. One list, so nothing can contradict it; the cost is
* that a step which becomes final after the author has chosen does not appear by itself. When
* displayed it is kept to steps that still have a result on the canvas. */
shownResultIds?: string[];
Comment thread
mengw15 marked this conversation as resolved.
}

export function getDefaultFormBinding(): FormBindingConfig {
return { fields: [], resultOperatorIds: [] };
return { fields: [] };
}

/**
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/app/workspace/component/menu/menu.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { ExecutionState } from "../../types/execute-workflow.interface";
import { HeatmapView } from "../../service/heatmap/heatmap-scoring";
import { ComputingUnitState } from "../../../common/type/computing-unit-connection.interface";
import { mockPoint, mockScanPredicate } from "../../service/workflow-graph/model/mock-workflow-data";
import { saveAs } from "file-saver";
import { FileSaverService } from "../../../dashboard/service/user/file/file-saver.service";
import type { ModalOptions } from "ng-zorro-antd/modal";
import type { ComputingUnitSelectionComponent } from "../power-button/computing-unit-selection.component";
import { WorkflowContent } from "../../../common/type/workflow";
Expand All @@ -57,8 +57,6 @@ import { MockGuiConfigService } from "../../../common/service/gui-config.service
import { JupyterPanelService } from "../../service/jupyter-panel/jupyter-panel.service";
import type { Mocked } from "vitest";

vi.mock("file-saver", () => ({ saveAs: vi.fn() }));

describe("MenuComponent", () => {
let component: MenuComponent;
let fixture: ComponentFixture<MenuComponent>;
Expand Down Expand Up @@ -112,7 +110,6 @@ describe("MenuComponent", () => {
fixture = TestBed.createComponent(MenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
vi.mocked(saveAs).mockClear();
});

it("should create", () => {
Expand Down Expand Up @@ -525,6 +522,9 @@ describe("MenuComponent", () => {

describe("onClickExportWorkflow (save)", () => {
it("serializes the workflow content as JSON and downloads it under the workflow name", () => {
// Stubbed on the injected wrapper rather than by module-mocking file-saver: that CommonJS
// mock is order-sensitive under the unit-test builder and was failing on the Windows leg.
const saveAs = vi.spyOn(TestBed.inject(FileSaverService), "saveAs").mockImplementation(() => {});
const fakeContent = {
operators: [{ operatorID: "op1" }],
links: [],
Expand All @@ -537,7 +537,7 @@ describe("MenuComponent", () => {
component.onClickExportWorkflow();

expect(saveAs).toHaveBeenCalledTimes(1);
const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0] as [Blob, string];
const [blobArg, fileNameArg] = saveAs.mock.calls[0] as [Blob, string];
expect(fileNameArg).toBe("my-workflow.json");
expect(blobArg).toBeInstanceOf(Blob);
expect(blobArg.type).toBe("text/plain;charset=utf-8");
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/app/workspace/component/menu/menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { catchError, debounceTime, switchMap, tap } from "rxjs/operators";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { WorkflowUtilService } from "../../service/workflow-graph/util/workflow-util.service";
import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service";
import { saveAs } from "file-saver";
import { FileSaverService } from "../../../dashboard/service/user/file/file-saver.service";
import { NotificationService } from "src/app/common/service/notification/notification.service";
import { OperatorMenuService } from "../../service/operator-menu/operator-menu.service";
import { CoeditorPresenceService } from "../../service/workflow-graph/model/coeditor-presence.service";
Expand Down Expand Up @@ -188,7 +188,8 @@ export class MenuComponent implements OnInit, OnDestroy {
private computingUnitStatusService: ComputingUnitStatusService,
protected config: GuiConfigService,
private router: Router,
private jupyterPanelService: JupyterPanelService
private jupyterPanelService: JupyterPanelService,
private fileSaverService: FileSaverService
) {
workflowWebsocketService
.subscribeToEvent("ExecutionDurationUpdateEvent")
Expand Down Expand Up @@ -619,7 +620,10 @@ export class MenuComponent implements OnInit, OnDestroy {
const workflowContent: WorkflowContent = this.workflowActionService.getWorkflowContent();
const workflowContentJson = JSON.stringify(workflowContent, null, 2);
const fileName = this.currentWorkflowName + ".json";
saveAs(new Blob([workflowContentJson], { type: "text/plain;charset=utf-8" }), fileName);
// Through the injectable wrapper (as the dashboard downloads already do), so a spec stubs it
// with TestBed instead of module-mocking the CommonJS file-saver package, which the unit-test
// builder cannot hoist reliably.
this.fileSaverService.saveAs(new Blob([workflowContentJson], { type: "text/plain;charset=utf-8" }), fileName);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,15 +235,16 @@ export class PropertyEditorComponent implements OnInit, OnDestroy, OnChanges {
ngOnDestroy(): void {
// The Form View's read-only copy (persistPlacement=false) must not persist geometry: it is not
// the docked canvas panel, so writing these keys would overwrite the real panel's saved size.
if (!this.persistPlacement) {
return;
}
localStorage.setItem("right-panel-width", String(this.width));
localStorage.setItem("right-panel-height", String(this.height));
// Guarding the block rather than returning early keeps any teardown added below it running for
// both mounts.
if (this.persistPlacement) {
localStorage.setItem("right-panel-width", String(this.width));
localStorage.setItem("right-panel-height", String(this.height));

const rightContainer = document.getElementById("right-container");
if (rightContainer) {
localStorage.setItem("right-panel-style", rightContainer.style.cssText);
const rightContainer = document.getElementById("right-container");
if (rightContainer) {
localStorage.setItem("right-panel-style", rightContainer.style.cssText);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
under the License.
-->

<!-- Commands that re-shape the graph (cut, paste, delete, disable/enable) are gated on canModify:
modification enabled AND no structure lock from an embedded preview. Copy, the result toggles,
execute-to and export do not re-shape it and follow their own rules. -->
<ul nz-menu>
<li
nz-menu-item
Expand All @@ -35,7 +38,7 @@
*ngIf="(highlightedOperatorIds.length > 0 ||
highlightedCommentBoxIds.length > 0) &&
!hasHighlightedLinks() &&
isWorkflowModifiable"
canModify"
(click)="onCut()">
<span
nz-icon
Expand All @@ -48,7 +51,7 @@
*ngIf="(highlightedOperatorIds.length === 0 &&
highlightedCommentBoxIds.length === 0 &&
!hasHighlightedLinks()) &&
isWorkflowModifiable"
canModify"
(click)="onPaste()">
<span
nz-icon
Expand All @@ -59,7 +62,8 @@
<li
nz-menu-item
*ngIf="operatorMenuService.isDisableOperator && operatorMenuService.isDisableOperatorClickable &&
!hasHighlightedLinks()"
!hasHighlightedLinks() &&
!structureLocked"
(click)="operatorMenuService.disableHighlightedOperators()">
<span
nz-icon
Expand All @@ -70,7 +74,8 @@
<li
nz-menu-item
*ngIf="!operatorMenuService.isDisableOperator && operatorMenuService.isDisableOperatorClickable &&
!hasHighlightedLinks()"
!hasHighlightedLinks() &&
!structureLocked"
(click)="operatorMenuService.disableHighlightedOperators()">
<span
nz-icon
Expand Down Expand Up @@ -127,7 +132,7 @@
nz-menu-item
*ngIf="(highlightedOperatorIds.length > 0 ||
highlightedCommentBoxIds.length > 0) &&
isWorkflowModifiable"
canModify"
(click)="onDelete()">
<span
nz-icon
Expand All @@ -142,7 +147,7 @@
*ngIf="hasHighlightedLinks() &&
highlightedOperatorIds.length === 0 &&
highlightedCommentBoxIds.length === 0 &&
isWorkflowModifiable"
canModify"
(click)="onDelete()">
<span
nz-icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,63 @@ describe("ContextMenuComponent", () => {
expect(spy).toHaveBeenCalledTimes(1);
});

// The Form View's edit mode turns modification on for the property panel while its embedded
// preview stays structure-locked; the lock has to reach the menu, or right-click on the preview
// could still re-shape the graph.
describe("under a structure lock", () => {
const renderedLabels = () =>
fixture.debugElement.queryAll(By.css("li[nz-menu-item]")).map(li => norm(li.nativeElement.textContent));

it("keeps cut, delete, disable and enable off even with modification enabled", () => {
highlightedOperatorsSubject.next(["op1"]);
component.isWorkflowModifiable = true;
component.structureLocked = true;
operatorMenuService.isDisableOperator = true;
operatorMenuService.isDisableOperatorClickable = true;
operatorMenuService.isToViewResult = true;
operatorMenuService.isToViewResultClickable = true;
fixture.detectChanges();

const labels = renderedLabels();
expect(labels).not.toContain("cut");
expect(labels).not.toContain("delete");
expect(labels).not.toContain("disable");
// Copying and the result toggle do not re-shape the graph, so they stay.
expect(labels).toContain("copy");
expect(labels).toContain("view result");

operatorMenuService.isDisableOperator = false;
fixture.detectChanges();
expect(renderedLabels()).not.toContain("enable");
});

it("keeps paste and link deletion off", () => {
component.isWorkflowModifiable = true;
component.structureLocked = true;
highlightedOperatorsSubject.next([]);
highlightedCommentBoxesSubject.next([]);
fixture.detectChanges();
expect(renderedLabels()).not.toContain("paste");

jointGraphWrapperSpy.getCurrentHighlightedLinkIDs.mockReturnValue(["link1"]);
fixture.detectChanges();
expect(renderedLabels()).not.toContain("delete");
});

it("canModify needs modification on and no structure lock", () => {
component.isWorkflowModifiable = true;
component.structureLocked = false;
expect(component.canModify).toBe(true);

component.structureLocked = true;
expect(component.canModify).toBe(false);

component.structureLocked = false;
component.isWorkflowModifiable = false;
expect(component.canModify).toBe(false);
});
});

it("execute to this operator invokes executeUpToOperator", () => {
highlightedOperatorsSubject.next(["op1"]);
component.isWorkflowModifiable = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { Component } from "@angular/core";
import { Component, Input } from "@angular/core";
import { OperatorMenuService } from "src/app/workspace/service/operator-menu/operator-menu.service";
import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
Expand All @@ -41,6 +41,20 @@ import { NzIconDirective } from "ng-zorro-antd/icon";
})
export class ContextMenuComponent {
public isWorkflowModifiable: boolean = false;

/**
* Set by an embedded, structure-locked editor (the Form View's preview). Its edit mode turns
* workflow modification back on for the property panel, which alone would also bring the
* re-shaping commands here back; the lock keeps them off. Copy, the result toggles and export
* do not change the graph and are left to their own rules.
*/
@Input() structureLocked = false;

/** Whether the graph may be re-shaped from this menu: modification on, and no structure lock. */
public get canModify(): boolean {
return this.isWorkflowModifiable && !this.structureLocked;
}

public highlightedOperatorIds: readonly string[] = [];
public highlightedCommentBoxIds: readonly string[] = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<nz-dropdown-menu
nzNoAnimation
#menu="nzDropdownMenu">
<texera-context-menu></texera-context-menu>
<texera-context-menu [structureLocked]="structureLocked"></texera-context-menu>
</nz-dropdown-menu>

<!-- Chat Popover for Operator -->
Expand Down
Loading
Loading