Show navigationHide navigation

Inline editing & overlays

Click a reference title or pencil icon to edit the linked entry inline. Overlays stack via repeated ?overlay=<content-path> URL params and round-trip cleanly when copied to a new window.

URL scheme#

Each overlay is a separate overlay query param carrying the full content path of the entry being edited:

/cms/content/blog/blog-2a77.json
  ?overlay=cms/content/post/post-x.json
  &overlay=cms/content/author/author-y.json

The stack is fully derived from the URL — useEntryStack reads searchParams.getAll('overlay') synchronously, so direct visit / new-window paste hydrates the entire stack on first render with no placeholder state and no init effect. The id and type are parsed from each path on the fly via the regex in octocms/hooks/useEntryStack.tsx.

EntryStackProvider wraps that logic in <Suspense fallback={null}> because useSearchParams() can suspend during App Router static-shell / hydration; the boundary lives in useEntryStack.tsx so entry editors do not each re-implement it.

Back button & close#

ActionImplementation
pushEntry(entry)router.push('?overlay=<path>') — appends a history entry.
popEntry()router.replace(...) with the last overlay= param removed. Works for both in-app pops and direct-visit deep links (where there is no in-tab history to pop).
closeAll()router.replace(...) with every overlay= param removed.

The in-app Back button in the overlay header calls popEntry()router.replace. The browser back button also closes one overlay, naturally, because every pushEntry did a router.push and history pops one entry at a time. We avoid router.back() so deep-link visits (where no prior in-tab history exists) still close cleanly via the in-app button.

Cycle detection#

pushEntry rejects any path already in ancestorPaths (a Set<string> of every entry currently in the stack, root included). Paths are globally unique; ids alone are not, since two collections may share an id.

Cross-pane sync — refreshTick#

When an inline overlay edits an entry the parent is showing (e.g. a blog post's authors reference list shows the author whose name was just edited), the parent's view needs to refresh once the overlay closes. We do this through a single context counter:

ts
const { refreshTick, bumpRefresh } = useEntryStack();
StepMechanism
1. Save / publish / archive / restore / delete inside an overlay succeedsdirtyRef.current = true; router.refresh() re-runs server components
2. Overlay closes (Back button or browser back)If dirtyRef.current, call bumpRefresh() once
3. refreshTick incrementsSubscribers re-run their data effects

Subscribers include refreshTick in their useEffect deps — no DOM events, no listeners, no payload casts.

Subscribers#

ComponentWhat it does on tick bump
octocms/components/ui/FormField/FormReferenceField.tsxRe-resolves titles for the items currently in its list via getEntryList(...). Items whose entry no longer appears in any allowed collection are dropped from the list (handles delete). User reordering / additions are preserved.
octocms/components/LinkedBySection/LinkedBySection.tsxRe-fetches getEntryBacklinks(referenceKey).
octocms/components/HistorySection/HistorySection.tsxRe-fetches getEntryCommits(entryPath) — but only if the section was already intersected (preserves the lazy-load rule; a tick fired before the user scrolled the section into view does not pre-fetch).

What was removed#

  • Legacy cross-pane window CustomEvent syncing for entry lifecycle — replaced by refreshTick plus TanStack Query invalidation elsewhere in the admin.
  • The ?editing=id1,id2 URL scheme is gone — IDs alone could not rehydrate the overlay (InlineEntryEditor needs entryPath + entryType), so deep URLs silently failed to restore. Paths fix this.
  • Placeholder hydration in EntryStackProvider is gone — the stack is derived directly from searchParams.getAll('overlay').
  • router.replace for stack changes is gone — router.push adds proper history entries so the browser back button works.

Verifying#

End-to-end smoke:

  1. Open an entry with a reference field.
  2. Click a reference → overlay opens; URL gains ?overlay=….
  3. Click another reference inside the overlay → URL gains a second &overlay=….
  4. Copy the URL into a new window → both overlays render.
  5. Press browser back twice → overlays close one at a time; URL drops one overlay= per press.
  6. Save a title change in an overlay; close. Parent's reference list shows the new title.
  7. Confirm the parent list updated via refreshTick / refetch — there is no window-level listener bus for this flow.

Unit tests: octocms/hooks/useEntryStack.test.tsx, octocms/components/InlineEntryEditor/InlineEntryEditor.test.tsx, octocms/components/ui/FormField/FormReferenceField.test.tsx, octocms/components/LinkedBySection/LinkedBySection.test.tsx, octocms/components/HistorySection/HistorySection.test.tsx.