diff --git a/src/main/frontend/components/grid-stack.ts b/src/main/frontend/components/grid-stack.ts index 503803a..554d29f 100644 --- a/src/main/frontend/components/grid-stack.ts +++ b/src/main/frontend/components/grid-stack.ts @@ -20,7 +20,12 @@ class GridStackLayout extends HTMLElement { disconnectedCallback() { this.observer?.disconnect(); 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 = undefined; } @@ -99,16 +104,28 @@ class GridStackLayout extends HTMLElement { private schedulePersist() { 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; const nodes = this.grid.save(false) as GridStackNode[]; if (this.storageKey) { 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() { @@ -122,13 +139,22 @@ class GridStackLayout extends HTMLElement { } catch { return; } + if (!Array.isArray(nodes)) return; - for (const node of nodes) { - if (!node.id) continue; - const el = this.querySelector(`:scope > [gs-id="${node.id}"]`) as HTMLElement | null; - if (!el) continue; // item no longer present server-side — skip, degrade gracefully - this.grid.update(el, { x: node.x, y: node.y, w: node.w, h: node.h }); - } + // Items dropped server-side since the layout was saved are skipped, so a + // stale entry degrades gracefully instead of blocking the restore. + const known = nodes.filter((node) => node.id + && this.querySelector(`:scope > [gs-id="${CSS.escape(String(node.id))}"]`)); + 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 { diff --git a/src/main/java/com/example/components/GridStackLayout.java b/src/main/java/com/example/components/GridStackLayout.java index 152ce62..d66deee 100644 --- a/src/main/java/com/example/components/GridStackLayout.java +++ b/src/main/java/com/example/components/GridStackLayout.java @@ -179,18 +179,17 @@ public class GridStackLayout extends Component implements HasSize, HasStyle { String id = node.path("id").asString(null); if (id == null) continue; - findItem(id).ifPresent(item -> item.setPosition( - node.path("x").asInt(), - node.path("y").asInt(), - node.path("w").asInt(), - node.path("h").asInt())); + // gridstack omits w/h from its saved layout when they are 1, so a + // missing value means 1 — defaulting to 0 wrote a zero-sized widget + // back onto the item and lost its size on the next attach. + int x = node.path("x").asInt(0); + 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( - id, - node.path("x").asInt(), - node.path("y").asInt(), - node.path("w").asInt(), - node.path("h").asInt())); + findItem(id).ifPresent(item -> item.setPosition(x, y, w, h)); + + positions.add(new GridStackItem.Position(id, x, y, w, h)); } fireEvent(new LayoutChangeEvent(this, true, positions));