mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: materialize empty wall catalog for new spaces
Issue: #324 User-Visible: yes
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -41,6 +41,8 @@ const res = await page.evaluate(async () => {
|
||||
await c._saveSpaceDialog(); await c.updateComplete;
|
||||
const mixed = c._serverCfg.spaces.find((s) => s.title === 'Mixed display defaults');
|
||||
out.savedMixed = [mixed?.settings?.show_borders, mixed?.settings?.show_names];
|
||||
out.savedMixedHasEmptyWallCatalog = Array.isArray(mixed?.wall_segments)
|
||||
&& mixed.wall_segments.length === 0;
|
||||
c._openSpaceDialog('edit', mixed.id); await c.updateComplete;
|
||||
out.reopenedMixed = state();
|
||||
await choose('file');
|
||||
@@ -78,6 +80,38 @@ const res = await page.evaluate(async () => {
|
||||
const file = c._serverCfg.spaces.find((s) => s.title === 'File display defaults');
|
||||
out.savedFileDefaults = [file?.settings?.show_borders, file?.settings?.show_names];
|
||||
|
||||
// #324: a server-rejected create must not survive merely because the dialog
|
||||
// mutated the reactive config before config/set. Reproduce invalid_format,
|
||||
// then let config/get return the last accepted document.
|
||||
const serverTruth = structuredClone(c._serverCfg);
|
||||
const baseCall = c.hass.callWS.bind(c.hass);
|
||||
let rejectedCandidate = null;
|
||||
let authoritativeReads = 0;
|
||||
c.hass = { ...c.hass, callWS: async (message) => {
|
||||
if (message.type === 'houseplan/config/set') {
|
||||
rejectedCandidate = structuredClone(message.config);
|
||||
const error = new Error('v8+ space requires wall_segments');
|
||||
error.code = 'invalid_format';
|
||||
throw error;
|
||||
}
|
||||
if (message.type === 'houseplan/config/get') {
|
||||
authoritativeReads++;
|
||||
return { config: structuredClone(serverTruth), rev: c._cfgRev, can_write: true };
|
||||
}
|
||||
return baseCall(message);
|
||||
} };
|
||||
c._openSpaceDialog('create'); await c.updateComplete;
|
||||
await choose('draw');
|
||||
c._spaceDialog = { ...c._spaceDialog, title: 'Rejected ghost' };
|
||||
await c._saveSpaceDialog(); await c.updateComplete;
|
||||
const rejectedSpace = rejectedCandidate?.spaces?.find((s) => s.title === 'Rejected ghost');
|
||||
out.rejectedCandidateHasEmptyWallCatalog = Array.isArray(rejectedSpace?.wall_segments)
|
||||
&& rejectedSpace.wall_segments.length === 0;
|
||||
out.rejectedCreateReloadsServerTruth = authoritativeReads === 1
|
||||
&& !c._serverCfg.spaces.some((s) => s.title === 'Rejected ghost');
|
||||
out.rejectedCreateKeepsDialogForRetry = c._spaceDialog?.title === 'Rejected ghost'
|
||||
&& c._spaceDialog.busy === false;
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Creating a space no longer reports `v8+ space requires wall_segments` while
|
||||
leaving an unsaved ghost space on screen; a failed space save now restores
|
||||
the server-backed state before another create or delete
|
||||
([#324](https://github.com/Matysh/houseplan-card/issues/324)).
|
||||
|
||||
## v1.68.0 — 2026-08-27
|
||||
|
||||
- Structural plan edits now rely on stable wall-segment IDs: thickness, draft
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Создание пространства больше не выдаёт ошибку
|
||||
`v8+ space requires wall_segments` и не оставляет на экране несохранённое
|
||||
пространство-призрак; после неудачного сохранения состояние возвращается к
|
||||
серверному до следующего создания или удаления
|
||||
([#324](https://github.com/Matysh/houseplan-card/issues/324)).
|
||||
|
||||
## v1.68.0 — 2026-08-27
|
||||
|
||||
- Структурные изменения плана теперь опираются на стабильные ID сегментов стен:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"sourceFingerprint": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceFingerprint": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "41e3ba67f8db0e98f26f484293af83ef937c369ca5ca6a59a3350d8954c906f4"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
|
||||
"sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -93,7 +93,7 @@ import {
|
||||
} from './initial-load';
|
||||
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
|
||||
import {
|
||||
initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
|
||||
createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
|
||||
} from './space-dialog';
|
||||
import { mdiHomeCityOutline } from '@mdi/js';
|
||||
import {
|
||||
@@ -14978,14 +14978,7 @@ class HouseplanCard extends LitElement {
|
||||
const cfg = this._serverCfg!;
|
||||
let sp: any;
|
||||
if (d.mode === 'create') {
|
||||
sp = {
|
||||
id: spaceId,
|
||||
title: d.title.trim(),
|
||||
plan_url: null,
|
||||
|
||||
view_box: [0, 0, 1, 1],
|
||||
rooms: [],
|
||||
};
|
||||
sp = createEmptySpaceConfig(spaceId, d.title.trim());
|
||||
cfg.spaces.push(sp);
|
||||
} else {
|
||||
sp = cfg.spaces.find((x: any) => x.id === spaceId);
|
||||
@@ -15085,6 +15078,14 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
// The dialog edits the reactive config optimistically before config/set.
|
||||
// A rejected create used to leave that unsaved space on screen; the next
|
||||
// delete then flushed the same malformed write and repeated its error.
|
||||
// Re-adopt server truth for every failed space transaction. Conflict is
|
||||
// already reloaded by _saveConfigNow; do not issue the same read twice.
|
||||
// A lost response is safe too: if the server did commit, config/get
|
||||
// returns it.
|
||||
if (e?.code !== 'conflict') await this._reloadConfigOnly(true);
|
||||
// audit L3: the dialog may have been closed (Esc) while the save was
|
||||
// in flight — spreading null yields a truthy husk and the renderer
|
||||
// then crashes, blanking the whole card. The toast below is the
|
||||
|
||||
@@ -8,6 +8,23 @@ export interface SpaceDisplayDraft {
|
||||
displayTouched: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A current empty space is still a complete wall-model document. In
|
||||
* particular, model v8+ forbids using an absent catalogue to mean "no walls":
|
||||
* absence means an outdated/partial writer, while an empty array is the
|
||||
* canonical empty catalogue.
|
||||
*/
|
||||
export function createEmptySpaceConfig(id: string, title: string): Record<string, unknown> {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
plan_url: null,
|
||||
view_box: [0, 0, 1, 1],
|
||||
rooms: [],
|
||||
wall_segments: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** Source-specific visible defaults for a fresh create/onboarding step. */
|
||||
export function initialSpaceDisplayDraft(source: SpacePlanSource = 'file'): SpaceDisplayDraft {
|
||||
const visible = source === 'draw';
|
||||
|
||||
@@ -2,11 +2,23 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
createEmptySpaceConfig,
|
||||
initialSpaceDisplayDraft,
|
||||
switchSpacePlanSource,
|
||||
touchSpaceDisplay,
|
||||
} from '../test-build/space-dialog.js';
|
||||
|
||||
test('a fresh space is a complete empty v8/v9 wall-model document (#324)', () => {
|
||||
assert.deepEqual(createEmptySpaceConfig('floor', 'Ground floor'), {
|
||||
id: 'floor',
|
||||
title: 'Ground floor',
|
||||
plan_url: null,
|
||||
view_box: [0, 0, 1, 1],
|
||||
rooms: [],
|
||||
wall_segments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('fresh create projects honest File and Draw display defaults', () => {
|
||||
const file = initialSpaceDisplayDraft();
|
||||
assert.deepEqual(file, {
|
||||
|
||||
@@ -1718,6 +1718,12 @@ def test_zero_wall_style_and_v9_legacy_fields_are_strict():
|
||||
"spaces": [{**base, "wall_segments": []}],
|
||||
}
|
||||
assert v.CONFIG_SCHEMA(canonical) == canonical
|
||||
with pytest.raises(vol.Invalid, match=r"v8\+ space requires wall_segments"):
|
||||
v.CONFIG_SCHEMA({
|
||||
**canonical,
|
||||
"spaces": [{key: value for key, value in canonical["spaces"][0].items()
|
||||
if key != "wall_segments"}],
|
||||
})
|
||||
with pytest.raises(vol.Invalid, match="legacy open boundaries"):
|
||||
v.CONFIG_SCHEMA({
|
||||
**canonical,
|
||||
|
||||
Reference in New Issue
Block a user