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#
| Action | Implementation |
|---|---|
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:
tsconst { refreshTick, bumpRefresh } = useEntryStack();
| Step | Mechanism |
|---|---|
| 1. Save / publish / archive / restore / delete inside an overlay succeeds | dirtyRef.current = true; router.refresh() re-runs server components |
| 2. Overlay closes (Back button or browser back) | If dirtyRef.current, call bumpRefresh() once |
3. refreshTick increments | Subscribers re-run their data effects |
Subscribers include refreshTick in their useEffect deps — no DOM events, no listeners, no payload casts.
Subscribers#
| Component | What it does on tick bump |
|---|---|
octocms/components/ui/FormField/FormReferenceField.tsx | Re-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.tsx | Re-fetches getEntryBacklinks(referenceKey). |
octocms/components/HistorySection/HistorySection.tsx | Re-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
windowCustomEvent syncing for entry lifecycle — replaced byrefreshTickplus TanStack Query invalidation elsewhere in the admin. - The
?editing=id1,id2URL scheme is gone — IDs alone could not rehydrate the overlay (InlineEntryEditorneedsentryPath+entryType), so deep URLs silently failed to restore. Paths fix this. - Placeholder hydration in
EntryStackProvideris gone — the stack is derived directly fromsearchParams.getAll('overlay'). router.replacefor stack changes is gone —router.pushadds proper history entries so the browser back button works.
Verifying#
End-to-end smoke:
- Open an entry with a reference field.
- Click a reference → overlay opens; URL gains
?overlay=…. - Click another reference inside the overlay → URL gains a second
&overlay=…. - Copy the URL into a new window → both overlays render.
- Press browser back twice → overlays close one at a time; URL drops one
overlay=per press. - Save a title change in an overlay; close. Parent's reference list shows the new title.
- Confirm the parent list updated via
refreshTick/ refetch — there is nowindow-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.
Related#
- docs/fields/reference.md — reference field configuration.
- docs/admin-routing.md — admin route dispatch model.
- docs/error-handling.md —
<ErrorBoundary>wrappers around inline-editor surfaces.