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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Graph } from "../../../../graph";
import { GraphMouseEvent } from "../../../../graphEvents";
import { Component } from "../../../../lib/Component";
import { GraphComponent } from "../../GraphComponent";

import { PortConnectionLayer } from "./PortConnectionLayer";
Expand All @@ -15,6 +16,9 @@ function mouse(type: string, x: number, y: number): MouseEvent {
}

type TSetup = {
graph: Graph;
layer: PortConnectionLayer;
onStart: jest.Mock;
onCancel: jest.Mock;
onDrop: jest.Mock;
/** Presses on the port without moving: the drag has not started yet */
Expand Down Expand Up @@ -42,6 +46,8 @@ function setup(): TSetup {

const onCancel = jest.fn();
const onDrop = jest.fn();
const onStart = jest.fn();
graph.on("port-connection-create-start", onStart, { signal: events.signal });
graph.on("port-connection-cancel", onCancel, { signal: events.signal });
graph.on("port-connection-create-drop", onDrop, { signal: events.signal });

Expand All @@ -68,10 +74,77 @@ function setup(): TSetup {
graph.detach();
};

return { onCancel, onDrop, press, startConnection, cancel, dispose };
return { graph, layer, onStart, onCancel, onDrop, press, startConnection, cancel, dispose };
}

describe("PortConnectionLayer cancellation", () => {
it.each([
["disabled", ({ layer }: TSetup) => layer.disable()],
["detached", ({ graph, layer }: TSetup) => graph.detachLayer(layer)],
["unmounted", ({ layer }: TSetup) => Component.unmount(layer)],
] as const)("stops a pending press when the layer is %s", (_reason, stop) => {
const fixture = setup();
const { press, onStart, onCancel, onDrop, dispose } = fixture;

press();
stop(fixture);
document.dispatchEvent(mouse("mousemove", 100, 100));
document.dispatchEvent(mouse("mouseup", 100, 100));

expect(onStart).not.toHaveBeenCalled();
expect(onCancel).not.toHaveBeenCalled();
expect(onDrop).not.toHaveBeenCalled();
dispose();
});

it("cancels an active drag on detach and observes settings after reattachment", () => {
const { graph, onStart, onCancel, onDrop, startConnection, dispose } = setup();
const settings = graph.rootStore.settings;
settings.setupSettings({ canCreateNewConnections: true });

startConnection();
graph.stop();
document.dispatchEvent(mouse("mouseup", 200, 200));

expect(onCancel).toHaveBeenCalledTimes(1);
expect(onDrop).not.toHaveBeenCalled();

graph.start();
startConnection();
settings.setupSettings({ canCreateNewConnections: false });
document.dispatchEvent(mouse("mouseup", 200, 200));
startConnection();
document.dispatchEvent(mouse("mouseup", 200, 200));

expect(onStart).toHaveBeenCalledTimes(2);
expect(onCancel).toHaveBeenCalledTimes(2);
expect(onDrop).not.toHaveBeenCalled();
dispose();
});

it("skips queued iterations after final unmount", () => {
const { graph, layer, dispose } = setup();

graph.detachLayer(layer);
Component.unmount(layer);

expect(layer.iterate()).toBe(false);
dispose();
});

it("preserves explicit enablement on reattachment with unchanged settings", () => {
const { graph, onStart, onDrop, startConnection, dispose } = setup();

graph.stop();
graph.start();
startConnection();
document.dispatchEvent(mouse("mouseup", 200, 200));

expect(onStart).toHaveBeenCalledTimes(1);
expect(onDrop).toHaveBeenCalledTimes(1);
dispose();
});

it("cancels a new connection on Escape without dropping it", () => {
const { onCancel, onDrop, startConnection, dispose } = setup();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ESelectionStrategy } from "../../../../services/selection";
import { EAnchorType } from "../../../../store/anchor/Anchor";
import { TBlockId } from "../../../../store/block/Block";
import { PortState } from "../../../../store/connection/port/Port";
import type { TGraphSettingsConfig } from "../../../../store/settings";
import type { Emitter } from "../../../../utils/Emitter";
import { getXY, vectorDistance } from "../../../../utils/functions";
import { stopDragListening } from "../../../../utils/functions/dragListener";
Expand Down Expand Up @@ -192,6 +193,8 @@ export class PortConnectionLayer extends Layer<
private portsUnsubscribe?: () => void;

protected enabled: boolean;
private lastSettings: TGraphSettingsConfig;
private disposed = false;

constructor(props: PortConnectionLayerProps) {
super({
Expand All @@ -214,14 +217,24 @@ export class PortConnectionLayer extends Layer<
graph: this.props.graph,
});

this.enabled = Boolean(this.props.graph.rootStore.settings.getConfigFlag("canCreateNewConnections"));
this.lastSettings = this.props.graph.rootStore.settings.$settings.value;
this.enabled = Boolean(this.lastSettings.canCreateNewConnections);
}

protected afterInit(): void {
this.onSignal(this.props.graph.rootStore.settings.$settings, (value) => {
this.enabled = Boolean(value.canCreateNewConnections);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Connection drags ignore current setting

When the setting changes while the layer is detached and remains unchanged after reattachment, afterInit only registers onSignal for future changes, so enabled remains stale; connection drags are accepted or rejected contrary to canCreateNewConnections.

On reattachment, reconcile enabled with the current setting when it differs from lastSettings, without overriding explicit enable/disable calls when the setting is unchanged.

Also at src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts:225-229.

Prompt for AI agents
In `src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts` at line 220:

**Connection drags ignore current setting**

When the setting changes while the layer is detached and remains unchanged after reattachment, `afterInit` only registers `onSignal` for future changes, so `enabled` remains stale; connection drags are accepted or rejected contrary to `canCreateNewConnections`.

On reattachment, reconcile `enabled` with the current setting when it differs from `lastSettings`, without overriding explicit enable/disable calls when the setting is unchanged.

Also at `src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts:225-229`.

// Subscribing again must preserve explicit enable/disable calls until the settings change.
if (value === this.lastSettings) {
return;
}
this.lastSettings = value;
if (value.canCreateNewConnections) {
this.enable();
} else {
this.disable();
}
});
}

protected afterInit(): void {
this.onGraphEvent("mousedown", this.handleMouseDown, { capture: true });

// Subscribe to ports changes
Expand All @@ -248,6 +261,7 @@ export class PortConnectionLayer extends Layer<

public disable = (): void => {
this.enabled = false;
this.cancelNewConnection();
};

protected currentListener: Emitter | null = null;
Expand Down Expand Up @@ -298,7 +312,10 @@ export class PortConnectionLayer extends Layer<
this.onStartConnection(port, new Point(coords[0], coords[1]));
},
onUpdate: (event, coords) => this.onMoveNewConnection(event, new Point(coords[0], coords[1])),
onEnd: (_event, coords) => this.onEndNewConnection(new Point(coords[0], coords[1])),
onEnd: (_event, coords) => {
this.currentListener = null;
this.onEndNewConnection(new Point(coords[0], coords[1]));
},
onCancel: () => this.onCancelNewConnection(),
},
{ cursor: "crosshair", initialEvent: initEvent }
Expand Down Expand Up @@ -474,8 +491,10 @@ export class PortConnectionLayer extends Layer<
* A press that has not started dragging yet is dropped without an event; after the drag ends it does nothing.
*/
protected cancelNewConnection(): void {
if (this.currentListener) {
stopDragListening(this.currentListener);
const listener = this.currentListener;
this.currentListener = null;
if (listener) {
stopDragListening(listener);
}
}

Expand Down Expand Up @@ -778,7 +797,17 @@ export class PortConnectionLayer extends Layer<
return {};
}

public override iterate(): boolean {
return this.disposed ? false : super.iterate();
}

protected override unmountLayer(): void {
this.cancelNewConnection();
super.unmountLayer();
}

public override unmount(): void {
this.disposed = true;
if (this.portsUnsubscribe) {
this.portsUnsubscribe();
this.portsUnsubscribe = undefined;
Expand Down
Loading