fix: restore saved gridstack layout faithfully (#13) #15

Merged
pitfriedrich merged 1 commits from ai/issue-13-persist-layout into main 2026-07-26 18:07:49 +00:00
2 changed files with 46 additions and 21 deletions
+36 -10
View File
@@ -20,7 +20,12 @@ class GridStackLayout extends HTMLElement {
disconnectedCallback() { disconnectedCallback() {
this.observer?.disconnect(); this.observer?.disconnect();
this.observer = undefined; this.observer = undefined;
clearTimeout(this.persistTimer); // Write out a still-pending debounced save instead of dropping it:
// a drag/resize followed straight away by navigating to another route
// detaches this element inside the debounce window, which used to lose
// the very last layout change. The server is not notified — its view is
// going away with us.
this.flushPersist();
this.grid?.destroy(false); // keep DOM — Flow owns the children this.grid?.destroy(false); // keep DOM — Flow owns the children
this.grid = undefined; this.grid = undefined;
} }
@@ -99,16 +104,28 @@ class GridStackLayout extends HTMLElement {
private schedulePersist() { private schedulePersist() {
clearTimeout(this.persistTimer); clearTimeout(this.persistTimer);
this.persistTimer = setTimeout(() => this.persist(), 150); this.persistTimer = setTimeout(() => {
this.persistTimer = undefined;
this.persist();
}, 150);
} }
private persist() { private flushPersist() {
if (this.persistTimer === undefined) return;
clearTimeout(this.persistTimer);
this.persistTimer = undefined;
this.persist(false);
}
private persist(notifyServer = true) {
if (!this.grid) return; if (!this.grid) return;
const nodes = this.grid.save(false) as GridStackNode[]; const nodes = this.grid.save(false) as GridStackNode[];
if (this.storageKey) { if (this.storageKey) {
localStorage.setItem(STORAGE_PREFIX + this.storageKey, JSON.stringify(nodes)); localStorage.setItem(STORAGE_PREFIX + this.storageKey, JSON.stringify(nodes));
} }
(this as any).$server?.onLayoutChange(JSON.stringify(nodes)); if (notifyServer) {
(this as any).$server?.onLayoutChange(JSON.stringify(nodes));
}
} }
private restore() { private restore() {
@@ -122,13 +139,22 @@ class GridStackLayout extends HTMLElement {
} catch { } catch {
return; return;
} }
if (!Array.isArray(nodes)) return;
for (const node of nodes) { // Items dropped server-side since the layout was saved are skipped, so a
if (!node.id) continue; // stale entry degrades gracefully instead of blocking the restore.
const el = this.querySelector(`:scope > [gs-id="${node.id}"]`) as HTMLElement | null; const known = nodes.filter((node) => node.id
Review

Excellent fix 👍

Excellent fix 👍
if (!el) continue; // item no longer present server-side — skip, degrade gracefully && this.querySelector(`:scope > [gs-id="${CSS.escape(String(node.id))}"]`));
this.grid.update(el, { x: node.x, y: node.y, w: node.w, h: node.h }); if (!known.length) return;
}
// load() rather than a per-item update() loop: it sorts the saved nodes,
// pulls them out of the engine before re-placing them, and does the whole
// thing in one batch. Updating item by item let a later item collide with
// one already put back and push it off its saved spot for good — the
// restored layout was then not the one the user left. load() also re-applies
// gridstack's w/h defaults, which save() omits when they are 1.
// `false` keeps add/remove off: Flow owns which children exist.
this.grid.load(known, false);
} }
private intAttr(el: HTMLElement, name: string): number | undefined { private intAttr(el: HTMLElement, name: string): number | undefined {
@@ -179,18 +179,17 @@ public class GridStackLayout extends Component implements HasSize, HasStyle {
String id = node.path("id").asString(null); String id = node.path("id").asString(null);
if (id == null) continue; if (id == null) continue;
findItem(id).ifPresent(item -> item.setPosition( // gridstack omits w/h from its saved layout when they are 1, so a
node.path("x").asInt(), // missing value means 1 — defaulting to 0 wrote a zero-sized widget
node.path("y").asInt(), // back onto the item and lost its size on the next attach.
node.path("w").asInt(), int x = node.path("x").asInt(0);
node.path("h").asInt())); int y = node.path("y").asInt(0);
int w = node.path("w").asInt(1);
int h = node.path("h").asInt(1);
positions.add(new GridStackItem.Position( findItem(id).ifPresent(item -> item.setPosition(x, y, w, h));
id,
node.path("x").asInt(), positions.add(new GridStackItem.Position(id, x, y, w, h));
node.path("y").asInt(),
node.path("w").asInt(),
node.path("h").asInt()));
} }
fireEvent(new LayoutChangeEvent(this, true, positions)); fireEvent(new LayoutChangeEvent(this, true, positions));