Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,38 @@ describe("OperatorPropertyEditFrameComponent", () => {
expect(component).toBeTruthy();
});

it("broadcasts currentlyEditing and syncs the operator version by default when an operator opens", () => {
workflowActionService.addOperator(mockScanPredicate, mockPoint);
const spy = vi.spyOn(workflowActionService.getTexeraGraph(), "updateSharedModelAwareness");
const versionSpy = vi.spyOn(workflowActionService, "setOperatorVersion");

component.ngOnChanges({
currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true),
});
fixture.detectChanges();

expect(spy).toHaveBeenCalledWith("currentlyEditing", mockScanPredicate.operatorID);
expect(versionSpy).toHaveBeenCalledWith(mockScanPredicate.operatorID, expect.anything());
});

it("neither broadcasts nor mutates the operator version when broadcastEditing is false (read-only inspect)", () => {
// The Form View mounts this frame with broadcastEditing=false so a reader inspecting a step is
// not announced as editing the graph, and opening the step does not write the operator version
// into the shared model.
workflowActionService.addOperator(mockScanPredicate, mockPoint);
component.broadcastEditing = false;
const spy = vi.spyOn(workflowActionService.getTexeraGraph(), "updateSharedModelAwareness");
const versionSpy = vi.spyOn(workflowActionService, "setOperatorVersion");

component.ngOnChanges({
currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true),
});
fixture.detectChanges();

expect(spy).not.toHaveBeenCalledWith("currentlyEditing", mockScanPredicate.operatorID);
expect(versionSpy).not.toHaveBeenCalled();
});

/**
* test if the property editor correctly receives the operator highlight stream,
* get the operator data (id, property, and metadata), and then display the form.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On
/** True while an author is choosing which properties appear on the Form View; adds a tick
* box beside each. Off, the property editor is unchanged. */
@Input() exposeChoosing = false;
/** Whether opening an operator may write to the shared workflow: the "currently editing" co-editor
* broadcast AND the operator-version sync (both are shared-model writes). True on the operator
* canvas; the Form View sets it false to inspect a step read-only, so opening one neither shows
* the reader as a co-editor nor mutates the operator's version. */
@Input() broadcastEditing = true;

currentOperatorSchema?: OperatorSchema;

Expand Down Expand Up @@ -631,10 +636,19 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On
this.currentOperatorSchema = this.dynamicSchemaService.getDynamicSchema(this.currentOperatorId);
this.currentOperatorStatus = this.workflowStatusSerivce.getCurrentStatus()[this.currentOperatorId];

this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("currentlyEditing", this.currentOperatorId);
if (this.broadcastEditing) {
this.workflowActionService
.getTexeraGraph()
.updateSharedModelAwareness("currentlyEditing", this.currentOperatorId);
}
const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);
// set the operator data needed
this.workflowActionService.setOperatorVersion(operator.operatorID, this.currentOperatorSchema.operatorVersion);
// Syncing the operator to the current schema version writes the new version into the Yjs shared
// model (changeOperatorVersion), which broadcasts and persists. That is right on the canvas, but
// a read-only inspect (broadcastEditing=false) must not mutate the workflow just by opening a
// step, so skip the sync there and show the version as stored.
if (this.broadcastEditing) {
this.workflowActionService.setOperatorVersion(operator.operatorID, this.currentOperatorSchema.operatorVersion);
}
this.operatorVersion = operator.operatorVersion.slice(0, 9);
this.setFormlyFormBinding(this.currentOperatorSchema.jsonSchema);
this.formTitle = operator.customDisplayName ?? this.currentOperatorSchema.additionalMetadata.userFriendlyName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ describe("PropertyEditorComponent", () => {
expect(component).toBeTruthy();
});

// The Form View mounts this panel with persistPlacement=false. It must not persist the docked
// canvas panel's geometry -- it is not that panel, and writing these keys would overwrite the
// real one's saved size. (The ngOnInit restore is guarded by the same flag; its canvas path is
// exercised by the default fixture above.)
it("does not persist the docked panel geometry when persistPlacement is false", () => {
component.persistPlacement = false;
const setItem = vi.spyOn(Storage.prototype, "setItem");

component.ngOnDestroy();

expect(setItem).not.toHaveBeenCalledWith("right-panel-width", expect.anything());
expect(setItem).not.toHaveBeenCalledWith("right-panel-style", expect.anything());
});

// The crash this flag fixes: ngOnInit reads #right-container to restore the docked panel's
// placement, and that element only exists in the canvas layout. The Form View mounts the panel
// with persistPlacement=false, where the element is absent -- reading it there would throw. With
// the flag off, ngOnInit must not go near it. Re-run ngOnInit on the existing instance (no second
// fixture, which would pollute TestBed) with the flag off and assert the lookup never happens.
it("does not read #right-container in ngOnInit when persistPlacement is false", () => {
component.persistPlacement = false;
const getById = vi.spyOn(document, "getElementById");

expect(() => component.ngOnInit()).not.toThrow();

expect(getById).not.toHaveBeenCalledWith("right-container");
});

/**
* test if the property editor correctly receives the operator unhighlight stream
* and clears all the operator data, and hide the form.
Expand All @@ -98,6 +126,7 @@ describe("PropertyEditorComponent", () => {
expect(component.componentInputs).toEqual({
currentOperatorId: mockScanPredicate.operatorID,
exposeChoosing: false,
broadcastEditing: true,
});

// unhighlight the operator
Expand Down Expand Up @@ -148,6 +177,7 @@ describe("PropertyEditorComponent", () => {
expect(component.componentInputs).toEqual({
currentOperatorId: mockScanPredicate.operatorID,
exposeChoosing: false,
broadcastEditing: true,
});

// unhighlight the operator
Expand All @@ -164,6 +194,7 @@ describe("PropertyEditorComponent", () => {
expect(component.componentInputs).toEqual({
currentOperatorId: mockResultPredicate.operatorID,
exposeChoosing: false,
broadcastEditing: true,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ export class PropertyEditorComponent implements OnInit, OnDestroy, OnChanges {
* Forwarded to the operator frame, which puts a tick box beside each property.
*/
@Input() exposeChoosing = false;
/**
* Whether this panel owns the docked canvas panel's saved size/position. The Form View mounts
* this same component read-only inside a preview box, where the canvas layout (#right-container)
* does not exist; with `false` it neither restores nor persists that shared placement, so it
* cannot crash on the missing element and cannot overwrite the canvas panel's geometry. Default
* `true` keeps the operator canvas exactly as it was.
*/
@Input() persistPlacement = true;
/**
* Whether opening an operator here broadcasts "currently editing this operator" on the shared
* co-editor channel. On the operator canvas that is right (a co-editor should see who is editing
* what). The Form View opens the panel only to inspect a step read-only, so it mounts with
* `false`: a reader is not editing the graph, and broadcasting would print the reader's own name
* in colour over that operator on everyone else's canvas. Default `true` keeps the canvas as it
* was. Forwarded to the operator frame, which owns the actual writes (the editing broadcast and
* the operator-version sync).
*/
@Input() broadcastEditing = true;
/** Set from the toolbar toggle on the operator canvas; the input covers the form view. */
private choosingFromToolbar = false;

Expand Down Expand Up @@ -137,11 +155,16 @@ export class PropertyEditorComponent implements OnInit, OnDestroy, OnChanges {
}

ngOnInit(): void {
const style = localStorage.getItem("right-panel-style");
if (style) document.getElementById("right-container")!.style.cssText = style;
const translates = document.getElementById("right-container")!.style.transform;
const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates);
this.returnPosition = { x: -xOffset, y: -yOffset };
// Restoring the docked panel's saved placement reads #right-container, which only exists in the
// canvas layout. The Form View mounts this panel with persistPlacement=false, where that element
// is absent, so skip the restore there (it would throw on the missing element).
if (this.persistPlacement) {
const style = localStorage.getItem("right-panel-style");
if (style) document.getElementById("right-container")!.style.cssText = style;
const translates = document.getElementById("right-container")!.style.transform;
const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates);
this.returnPosition = { x: -xOffset, y: -yOffset };
}
this.registerHighlightEventsHandler();
// The toolbar's "choose fields" toggle lives in the service so both the canvas toolbar
// and this panel see the same state. Re-emit the frame's inputs when it changes, so tick
Expand Down Expand Up @@ -205,6 +228,11 @@ export class PropertyEditorComponent implements OnInit, OnDestroy, OnChanges {

@HostListener("window:beforeunload")
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));

Expand Down Expand Up @@ -248,7 +276,11 @@ export class PropertyEditorComponent implements OnInit, OnDestroy, OnChanges {

if (highlightedOperators.length === 1 && highlightLinks.length === 0 && highlightedPorts.length === 0) {
this.currentComponent = OperatorPropertyEditFrameComponent;
this.componentInputs = { currentOperatorId: highlightedOperators[0], exposeChoosing: this.choosing };
this.componentInputs = {
currentOperatorId: highlightedOperators[0],
exposeChoosing: this.choosing,
broadcastEditing: this.broadcastEditing,
};
} else if (highlightedPorts.length === 1 && highlightLinks.length === 0) {
this.currentComponent = PortPropertyEditFrameComponent;
this.componentInputs = { currentPortID: highlightedPorts[0] };
Expand Down
Loading
Loading