diff --git a/frontend/src/app/common/type/workflow.ts b/frontend/src/app/common/type/workflow.ts index 1aa6b40878d..4129346b338 100644 --- a/frontend/src/app/common/type/workflow.ts +++ b/frontend/src/app/common/type/workflow.ts @@ -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[]; } export function getDefaultFormBinding(): FormBindingConfig { - return { fields: [], resultOperatorIds: [] }; + return { fields: [] }; } /** 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 c726bfec659..da69bf3adf0 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts @@ -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"; @@ -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; @@ -112,7 +110,6 @@ describe("MenuComponent", () => { fixture = TestBed.createComponent(MenuComponent); component = fixture.componentInstance; fixture.detectChanges(); - vi.mocked(saveAs).mockClear(); }); it("should create", () => { @@ -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: [], @@ -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"); diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index b5b209b3eb0..c3ad4aa979e 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -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"; @@ -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") @@ -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); } /** diff --git a/frontend/src/app/workspace/component/property-editor/property-editor.component.ts b/frontend/src/app/workspace/component/property-editor/property-editor.component.ts index 5952b942101..63b97a71c7a 100644 --- a/frontend/src/app/workspace/component/property-editor/property-editor.component.ts +++ b/frontend/src/app/workspace/component/property-editor/property-editor.component.ts @@ -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); + } } } 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 4465d65cb27..7d423f58d52 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 @@ -17,6 +17,9 @@ under the License. --> +