Skip to content

feat(LayoutBrowser): refetch organization layouts from the server - #8

Merged
rahulkatiyar19955 merged 2 commits into
mainfrom
feat/refetch-org-layout
Aug 9, 2026
Merged

feat(LayoutBrowser): refetch organization layouts from the server#8
rahulkatiyar19955 merged 2 commits into
mainfrom
feat/refetch-org-layout

Conversation

@rahulkatiyar19955

Copy link
Copy Markdown
Collaborator

Problem

Once an organization layout is loaded, there is no way to pull the server's current version. The periodic sync (30s–3min) does refresh baselines, but:

  • update-baseline preserves the local working copy, so as soon as a user nudges a panel their stale edit shadows the server version indefinitely.
  • CurrentLayoutProvider re-applies a layout to the viewport only on a revert event, so even a refreshed baseline stays invisible until the user switches layouts and back.

Change

LayoutManager.refetchLayout({ id }) replaces a shared layout's cached copy with the server's current version, discarding the working copy. It reports the result as a revert event, which is what makes the open layout actually re-render.

Two entry points:

  • "Refetch from server" in a layout row's context menu / ⋮ menu. Confirms first (danger dialog) only when the layout has unsaved changes. Shown for ORG_WRITE and read-only ORG_READ catalog layouts — refetching is a read, so unlike rename/delete it is not gated on write permission, and a stale catalog layout is exactly where a user gets stranded. Hidden on personal layouts, disabled offline and under multi-selection.
  • A refresh button in the Layouts sidebar header that force-runs syncWithRemote instead of waiting out the backoff. Deliberately non-destructive: it never discards anyone's working copy. Only rendered when organization layout storage is present.

The remote copy is located via getLayouts() rather than getLayout(id), since only some IRemoteLayoutStorage implementations support fetching a single layout by id.

refetchLayout and syncWithRemote are now declared on ILayoutManager (the latter already existed on the concrete class and the mock).

Testing

  • LayoutManager.refetchLayout: baseline replaced and working dropped, revert emitted, personal layout rejected, offline rejected, missing locally, deleted on the server.
  • LayoutRow: offered on catalog layouts, hidden for personal and when no handler is supplied, disabled offline / multi-select, confirm honored and dismissed.
  • LayoutBrowser and useLayoutActions: refresh button hidden without sharing, calls syncWithRemote, disabled offline.

270 tests pass across the affected suites; yarn build:packages and yarn extension:typecheck exit 0; eslint and biome are clean.

Organization layouts had no way to pull the server's current version. The
periodic sync refreshes baselines, but it preserves the local working copy and
only a revert event re-applies a layout to the viewport, so once a user touches
an org layout their stale copy shadows the server's indefinitely.

Add LayoutManager.refetchLayout, which replaces a shared layout's cached copy
with the server's and discards local changes. It reports the change as a revert
so CurrentLayoutProvider re-renders the open layout.

Surface it two ways:

- "Refetch from server" in a layout row's context menu, confirmed when the
  layout has unsaved changes. Offered on read-only catalog layouts too, since
  refetching is a read.
- A refresh button in the Layouts sidebar that force-runs syncWithRemote for
  everything, non-destructive and never discarding a working copy.

refetchLayout and syncWithRemote are now declared on ILayoutManager; the latter
already existed on the concrete class.
…etch

refetchLayout wrote syncInfo.status = "tracked" regardless of the permission
the server returned. A row that comes back as CREATOR_WRITE would then be
stored as a personal layout carrying syncInfo, and the sync reducer treats a
tracked layout missing from the remote list as delete-local — silently dropping
it from the cache.

Key the syncInfo off the returned permission, matching add-to-cache.
// Keyed off the permission the server just returned, not the one the
// layout had locally: a personal layout must never carry syncInfo, or
// the sync reducer becomes free to delete it. Same rule as add-to-cache.
syncInfo: layoutPermissionIsShared(remoteLayout.permission)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review finding (fixed in e2538da): refetchLayout originally wrote syncInfo: { status: "tracked", ... } unconditionally.

Failure scenario: the remote row comes back with permission: "CREATOR_WRITE" (someone demoted it, or the workspace list includes personal rows as it can in the web LayoutsAPI build). The layout was then persisted as a personal layout carrying syncInfo. LayoutBrowser files it under Personal, and on the next sync where that row is absent from the remote list, computeLayoutSyncOperations.syncLocalLayout hits case "tracked" with !layoutIsShared(localLayout) and emits delete-local — silently deleting the user's personal layout from the cache. This is the invariant updateLayout documents as "Personal layouts should NEVER have syncInfo".

Now keyed off the returned permission, matching the add-to-cache sync path.

name: remoteLayout.name,
permission: remoteLayout.permission,
baseline: { data: remoteLayout.data, savedAt: remoteLayout.savedAt },
working: undefined,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review finding (not fixed — known limitation, tracked here): clearing working races with CurrentLayoutSyncAdapter's 1s debounced write.

Failure scenario: user drags a panel in an org layout. performAction sets edited: true and the adapter starts its SAVE_INTERVAL_MS (1000ms) debounce. Because working has not been persisted yet, layout.working is still undefined, so LayoutRow's hasModifications is false and Refetch from server runs with no confirm dialog. The refetch clears working and the viewport redraws with the server copy. ~1s later the debounced useAsync flushes its pre-refetch snapshot through layoutManager.updateLayout({ id, data: staleEditedData }); that compares against the new server baseline, finds a difference, and writes the discarded edits back as working. The layout re-acquires the unsaved-changes dot and the discarded edits come back the next time it is loaded.

Not fixed in this PR deliberately: revertLayout has the identical exposure today, and the correct fix is in CurrentLayoutSyncAdapter (drop pending unsaved entries for a layout when a revert event arrives for it), which is outside this diff. Window is ~1s from the last panel edit.

@rahulkatiyar19955 rahulkatiyar19955 self-assigned this Aug 9, 2026
@rahulkatiyar19955
rahulkatiyar19955 merged commit 0e54037 into main Aug 9, 2026
3 of 4 checks passed
@rahulkatiyar19955
rahulkatiyar19955 deleted the feat/refetch-org-layout branch August 9, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant