From d433b64cf03b32bda0fb241cbdf28fe371d3780b Mon Sep 17 00:00:00 2001 From: Matysh Date: Tue, 8 Sep 2026 23:21:38 +0300 Subject: [PATCH] fix: keep ordinary marker dialog close synchronous User-Visible: no Issue: #485 --- src/houseplan-editor-runtime.ts | 22 ++++++++-------------- src/marker-dialog-close.ts | 22 ++++++++++++++++++++++ src/radar-setup.ts | 4 ++++ test/radar-setup.test.mjs | 5 ++++- 4 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 src/marker-dialog-close.ts diff --git a/src/houseplan-editor-runtime.ts b/src/houseplan-editor-runtime.ts index d88d5afc..3de739f5 100644 --- a/src/houseplan-editor-runtime.ts +++ b/src/houseplan-editor-runtime.ts @@ -190,6 +190,7 @@ import { radarAfterBindingChange, radarConfigFromDraft, radarDraft, recognizeRadar, type RadarEditorDraft, } from './radar-editor'; +import { finishMarkerDialogClose } from './marker-dialog-close'; import { renderRadarSection } from './editors/radar-section'; import { radarDiscardRequest, RadarSetupController } from './radar-setup'; import { @@ -7327,25 +7328,18 @@ public _openDeviceInbox(): void { }; } -public _closeMarkerDialog(): void { void this._closeMarkerDialogGuarded(); } +public _closeMarkerDialog(): void { + if (this._radarSetup.isDirty()) { void this._closeMarkerDialogGuarded(); return; } + this._radarSetup.reset(); + finishMarkerDialogClose(this.host); + } private async _closeMarkerDialogGuarded(): Promise { - if (!await this._radarSetup.discardIfAllowed()) { + if (!await this._radarSetup.discardIfAllowed()) { await this.host.updateComplete; this.host.renderRoot .querySelector('#marker-dialog')?.rejectClose(); return; } - this.host._markerDialog = null; - if (this.host._deviceInboxReturn) { - const restored = { ...this.host._deviceInboxReturn }; - this.host._deviceInbox = restored; - this.host._deviceInboxReturn = null; - if (restored.anchor) { - void this.host.updateComplete.then(() => requestAnimationFrame(() => { - const selector = `.device-inbox-row[data-binding="${CSS.escape(restored.anchor!)}"]`; - this.host.renderRoot.querySelector(selector)?.scrollIntoView({ block: 'nearest' }); - })); - } - } + this._closeMarkerDialog(); } public _deviceInboxCandidates(showEntities: boolean) { diff --git a/src/marker-dialog-close.ts b/src/marker-dialog-close.ts new file mode 100644 index 00000000..e597322e --- /dev/null +++ b/src/marker-dialog-close.ts @@ -0,0 +1,22 @@ +interface MarkerDialogCloseHost { + _markerDialog: unknown | null; + _deviceInboxReturn: T | null; + _deviceInbox: T | null; + updateComplete: Promise; + renderRoot: ParentNode; +} + +export function finishMarkerDialogClose( + host: MarkerDialogCloseHost, +): void { + host._markerDialog = null; + if (!host._deviceInboxReturn) return; + const restored = { ...host._deviceInboxReturn } as T; + host._deviceInbox = restored; + host._deviceInboxReturn = null; + if (!restored.anchor) return; + void host.updateComplete.then(() => requestAnimationFrame(() => { + const selector = `.device-inbox-row[data-binding="${CSS.escape(restored.anchor!)}"]`; + host.renderRoot.querySelector(selector)?.scrollIntoView({ block: 'nearest' }); + })); +} diff --git a/src/radar-setup.ts b/src/radar-setup.ts index c1114099..b1a10703 100644 --- a/src/radar-setup.ts +++ b/src/radar-setup.ts @@ -140,6 +140,10 @@ export class RadarSetupController { public isActive(): boolean { return this.active !== null; } + public isDirty(): boolean { + return !!this.active && (this.active.mount !== null || this.active.phase !== 'mount'); + } + public syncMarker(markerId: string): void { if (this.active && this.active.markerId !== markerId) this.reset(); } diff --git a/test/radar-setup.test.mjs b/test/radar-setup.test.mjs index f36f0698..16596c34 100644 --- a/test/radar-setup.test.mjs +++ b/test/radar-setup.test.mjs @@ -26,9 +26,11 @@ test('on-plan installation changes only the editor draft until ordinary Save', ( confirmDiscard: async () => true, }); assert.equal(controller.begin('radar', draft(), { - id: 'living', name: 'Living', poly: [[0, 0], [1, 0], [1, 1], [0, 1]], + id: 'living', name: 'Living', poly: [[0, 0], [1, 0], [1, 1], [0, 1]], }, 5, 7), true); + assert.equal(controller.isDirty(), false, 'an untouched setup may close synchronously'); controller.choosePoint(pointer(250, 400)); + assert.equal(controller.isDirty(), true, 'placing the mount makes the setup discard-sensitive'); controller.choosePoint(pointer(250, 100)); assert.equal(applied, null, 'the setup surface must not persist or apply before confirmation'); controller.apply(); @@ -54,6 +56,7 @@ test('cancelling calibration releases its draft subscription exactly once', asyn assert.equal(await controller.cancel(), true); assert.equal(await controller.cancel(), true); assert.equal(calls, 1); + assert.equal(controller.isDirty(), false); }); test('dirty calibration stays open when discard confirmation is rejected', async () => {