Skip to content

Live-refresh list windows on content changes (posts, CPTs, comments, WooCommerce orders) - #388

Merged
AllTerrainDeveloper merged 1 commit into
trunkfrom
feat/content-changed-live-refresh
Jul 22, 2026
Merged

Live-refresh list windows on content changes (posts, CPTs, comments, WooCommerce orders)#388
AllTerrainDeveloper merged 1 commit into
trunkfrom
feat/content-changed-live-refresh

Conversation

@AllTerrainDeveloper

@AllTerrainDeveloper AllTerrainDeveloper commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes #372

Problem

Adding or updating a WooCommerce order in one desktop window left an open Orders-list window stale. The gap generalizes: the shell already had all the consumer machinery — the cross-window broadcast bus (desktop-mode.<type>.changed), the iframe soft-reload, native list windows that subscribe — but nothing published those topics on a normal save. Only the recycle bin (trash verbs) and the native posts window's own mutations emitted.

Solution — a generic content-change realtime layer

Any create / update / trash of a post, page, show_ui CPT, comment, or WooCommerce order (HPOS and legacy, one shop_order topic for both) now reaches every window through three delivery paths:

  1. Chromeless footer (instant) — new includes/content-changes.php records mutations into a per-request changelog (wp_after_insert_post with revision/autosave/auto-draft/trash-status skips, comment hooks, woocommerce_* order hooks behind a class_exists guard). Because the dominant save flow is form-POST → 302 → GET, the changelog survives the redirect in a 60 s per-user transient flushed by the next chromeless admin_footer render (~500 ms after the click).
  2. Block editor (instant) — the bridge's existing core/editor save-watcher now also posts the broadcast upstream on save success (source: 'editor', created vs updated captured on the save-start tick). Gutenberg saves over REST, so no footer ever renders there.
  3. Heartbeat catch-all (≤ 1 tick) — every record is appended to a pruned _desktop_mode_content_changes_log option (autoload=false, 5-min window / 100 entries); opted-in shells re-broadcast fresh entries (source: 'heartbeat'). Covers Quick Edit, AJAX status flips, other browser tabs/users, REST/WP-CLI. The first tick is a pure handshake that adopts the server clock, so client/server skew can never drop changes. Tabs that never opt in pay zero per tick.

Consumers

  • The iframe soft-reload matcher is now generic: edit.php?post_type=X reacts to desktop-mode.X.changed for any post type with zero per-type code (also covers legacy edit.php?post_type=shop_order).
  • Declarative extras via the new desktop_mode_soft_reload_rules filter, shipping one rule for the HPOS orders list (admin.php?page=wc-orders). queryAbsent: [ 'action' ] is load-bearing — it keeps the single-order editor under the single-edit exclusion so a background refresh can never destroy unsaved order state.
  • The native Comments window now subscribes to desktop-mode.comment.changed and refreshes in place (the 30 s count-poll + reload pill stays as fallback).
  • Native Posts / Pages / Users windows already subscribed — they light up automatically.

Recycle-bin delegation

The bin's per-domain changelog now forwards into the generic recorder (desktop_mode_recycle_bin_record_change() kept as a thin wrapper), so a trash and a save flow through one footer emitter — each type/action pair broadcasts exactly once per render. The bin-specific desktop-mode-recycle-bin-changed ts signal, option, and heartbeat handler are unchanged. First-writer-wins dedupe per type:id keeps the more specific trash verb over the follow-up status-write updated.

New public surface (all documented)

  • desktop_mode_content_changes_record( $type, $id, $action ) — public recorder for plugins with custom storage
  • Filters: desktop_mode_content_changes_should_record, desktop_mode_content_change_topic, desktop_mode_content_changes_broadcasts, desktop_mode_soft_reload_rules
  • Actions: desktop_mode_content_change_recorded, desktop_mode_content_changes_emitted
  • Heartbeat fields: desktop_mode_content_changes_seen_ts (client → server), desktop_mode_content_changes (server → client)
  • Extended desktop-mode.<type>.changed payload contract: source gains 'editor' / 'heartbeat', action gains 'created' / 'updated'

Docs updated in the same change: hooks-reference.md (new section + rewritten cross-window broadcast contract), javascript-reference.md, bridge-protocol.md, api-index.md, new examples/content-changes.md recipe.

Tests

  • PHPUnit (tests/phpunit/tests/contentChanges.php, 22 cases): recorder validation + first-writer-wins dedupe, should_record veto, created-vs-updated (auto-draft first save), revision/show_ui skips, trash/untrash integration proving the recycle-bin delegation end-to-end, comment transitions, WC guard, redirect buffer, footer emitter (emit + buffer consumption + no-rebuffer + chromeless gate + suppress filter), heartbeat filter.
  • Vitest (tests/vitest/content-changes-heartbeat.test.ts, 6 cases): handshake semantics, re-broadcast fan-out, high-water-mark advance, malformed-entry hygiene.
  • Full suites green: 1269 PHP / 2108 JS; build + lint + typecheck clean.

Manual QA notes

Verified flows: HPOS order status change + new order (instant list refresh, no spinner), legacy WC, Gutenberg save → posts list + native Posts window, CPT saves, comment moderation, AJAX status flip via heartbeat, cross-tab catch-up, and the regression checks (single soft-reload fetch per change; dirty editors never background-reloaded).

Known accepted redundancy (documented): a change delivered instantly by the footer/editor path is re-broadcast once by the next heartbeat tick — consumers are idempotent, cost is one extra refresh fetch.

🤖 Generated with Claude Code

Open WordPress Playground Preview

Saving a post, page, CPT, comment, or WooCommerce order in one window
now refreshes every other window listing that type. Three delivery
paths feed the existing broadcast bus (desktop-mode.<type>.changed):

- Chromeless footer (instant): includes/content-changes.php records
  mutations into a per-request changelog (wp_after_insert_post,
  comment hooks, woocommerce_* order hooks for HPOS + legacy) and a
  60s per-user transient carries it across the POST -> 302 -> GET
  redirect to the next chromeless admin_footer render.
- Block editor (instant): the bridge save-watcher now also posts the
  broadcast upstream on save success (REST saves render no footer).
- Heartbeat catch-all (<= 1 tick): pruned changelog option + shell
  module re-broadcasting fresh entries; covers Quick Edit, AJAX
  status flips, other tabs/users, REST/WP-CLI. First tick is a
  clock-skew-proof handshake.

Consumers: the iframe soft-reload matcher is now generic (any
edit.php?post_type=X reacts to its own type) plus declarative extras
via the new desktop_mode_soft_reload_rules filter, shipping the HPOS
admin.php?page=wc-orders rule (queryAbsent action protects the order
editor). The recycle bin delegates its changelog into the generic
recorder (single emitter, no duplicate broadcasts) and the native
Comments window auto-refreshes on comment.changed.

New public surface: desktop_mode_content_changes_record(), filters
desktop_mode_content_changes_should_record / _change_topic /
_changes_broadcasts / desktop_mode_soft_reload_rules, actions
desktop_mode_content_change_recorded / _changes_emitted, heartbeat
fields desktop_mode_content_changes[_seen_ts]. Documented in
hooks-reference, javascript-reference, bridge-protocol, api-index,
and a new examples/content-changes.md recipe.

Tests: 22 PHPUnit + 6 vitest cases covering recorder dedupe, hook
wiring, the redirect buffer, footer emitter, heartbeat filter, and
the client handshake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AllTerrainDeveloper
AllTerrainDeveloper merged commit aceb795 into trunk Jul 22, 2026
5 checks passed
@AllTerrainDeveloper
AllTerrainDeveloper deleted the feat/content-changed-live-refresh branch July 22, 2026 12:04
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.

Add "Refresh" button to the Orders (Woo) window

1 participant