From 7842fa0173b9c1334c678c3d863f633fcb39b6f3 Mon Sep 17 00:00:00 2001 From: ella Date: Fri, 22 May 2026 11:41:50 +0200 Subject: [PATCH] Collab Notes: Defer the full comments fetch behind preloaded count probes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collab-notes hook fires `GET /wp/v2/comments?type=note&per_page=-1` on every editor mount, even when the post has no notes. The full response feeds two sidebar gates (All Notes sidebar presence, floating sidebar auto-open) — both of which only care about counts, not full threads. For the common case of a post with no notes, today we still pay the eager fetch. Replace with two small preloadable presence probes plus a gated full fetch: 1. `useNoteThreads` adds two `useEntityRecords` calls with `per_page=1&_fields=id`, one each for `status=all` and `status=hold`. Their `X-WP-Total` populates `notesCount` / `unresolvedCount`. 2. The full per-page fetch is gated on `notesCount > 0` — so posts with no notes never fire it. 3. `NotesSidebar` reads `hasNotes` / `hasUnresolvedNotes` from the hook instead of `notes.length` / `unresolvedNotes.length`, so the gates work even before the (now-conditional) full fetch resolves. 4. The two count URLs are added to `block_editor_rest_api_preload_paths` for the post-editor context, and driven in the kickoff alongside the other per-post resolvers so they consume the preload entries before `clearPreloadedData` runs. Empirical verification on a fresh draft (no notes): - Before: full `GET /wp/v2/comments?…&type=note&per_page=100` request fires post-mount. - After: `[api-fetch][preload] All preloads consumed.` (the count probes are cache-hits), and **no** `/wp/v2/comments` request fires. With at least one note on the post: - Count probes still cache-hit (same preload entries). - Full fetch fires the same as today — drives indicators + sidebar content rendering. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/compat/wordpress-7.0/preload.php | 34 ++++++++++++++++ packages/edit-post/src/index.js | 20 ++++++++++ .../src/components/collab-sidebar/hooks.js | 40 ++++++++++++++++++- .../src/components/collab-sidebar/index.js | 7 ++-- 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/lib/compat/wordpress-7.0/preload.php b/lib/compat/wordpress-7.0/preload.php index 49318230eb0810..92e522cfc48435 100644 --- a/lib/compat/wordpress-7.0/preload.php +++ b/lib/compat/wordpress-7.0/preload.php @@ -57,3 +57,37 @@ function gutenberg_block_editor_preload_paths_root_fields( $paths ) { return $paths; } add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_root_fields' ); + +/** + * Preload the notes-presence probes for the post editor. + * + * The collab notes feature drives sidebar visibility and the + * floating-sidebar auto-open from two count probes: + * - total note count (`status=all`) + * - unresolved count (`status=hold`) + * Each is a tiny list call (`per_page=1&_fields=id`) whose + * `X-WP-Total` header carries the count. Preloading them keeps the + * boot path cache-only — the full per-page comments fetch only fires + * when the total count is > 0. + * + * @param array $paths REST API paths to preload. + * @param WP_Block_Editor_Context $context Current block editor context. + * @return array Filtered preload paths. + */ +function gutenberg_block_editor_preload_paths_notes_counts( $paths, $context ) { + if ( 'core/edit-post' !== $context->name || ! isset( $context->post ) ) { + return $paths; + } + $post_id = (int) $context->post->ID; + if ( ! $post_id ) { + return $paths; + } + $base = sprintf( + '/wp/v2/comments?context=edit&post=%d&type=note&per_page=1&_fields=id', + $post_id + ); + $paths[] = $base . '&status=all'; + $paths[] = $base . '&status=hold'; + return $paths; +} +add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_notes_counts', 10, 2 ); diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 6f20909b9ec1d6..96aeb8bb1e8604 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -236,6 +236,26 @@ async function preloadResolutions( postType, postId ) { postId ), core.getAutosaves( postType, postId ), + // Collab notes presence probes — paired with the + // preload entries in `wordpress-7.0/preload.php`. + // The hook in `useNoteThreads` reads `totalItems` + // from these to drive the sidebar/floating gates + // without doing the full per-page comments fetch + // on every boot. + core.getEntityRecords( 'root', 'comment', { + post: postId, + type: 'note', + status: 'all', + per_page: 1, + _fields: 'id', + } ), + core.getEntityRecords( 'root', 'comment', { + post: postId, + type: 'note', + status: 'hold', + per_page: 1, + _fields: 'id', + } ), ] : [] ), ] ); diff --git a/packages/editor/src/components/collab-sidebar/hooks.js b/packages/editor/src/components/collab-sidebar/hooks.js index 2208946ba80f5d..826b8e13a56915 100644 --- a/packages/editor/src/components/collab-sidebar/hooks.js +++ b/packages/editor/src/components/collab-sidebar/hooks.js @@ -36,6 +36,39 @@ import { const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); export function useNoteThreads( postId ) { + const enabled = !! postId && typeof postId === 'number'; + + // Cheap presence probes: 1-record list calls whose response body is + // effectively empty but whose `X-WP-Total` header gives the exact + // count. The kickoff's PHP preload covers these (see + // `lib/compat/wordpress-7.0/preload.php`), so the boot path resolves + // them from cache. The counts then gate the heavier full fetch + // below. + const { totalItems: notesCount } = useEntityRecords( + 'root', + 'comment', + { + post: postId, + type: 'note', + status: 'all', + per_page: 1, + _fields: 'id', + }, + { enabled } + ); + const { totalItems: unresolvedCount } = useEntityRecords( + 'root', + 'comment', + { + post: postId, + type: 'note', + status: 'hold', + per_page: 1, + _fields: 'id', + }, + { enabled } + ); + const queryArgs = { post: postId, type: 'note', @@ -43,11 +76,14 @@ export function useNoteThreads( postId ) { per_page: -1, }; + // Full fetch only when there's at least one note. Posts with no + // notes pay just the cached count probes; posts with notes pay the + // same fetch they pay today. const { records: threads } = useEntityRecords( 'root', 'comment', queryArgs, - { enabled: !! postId && typeof postId === 'number' } + { enabled: enabled && ( notesCount ?? 0 ) > 0 } ); const { getBlockAttributes } = useSelect( blockEditorStore ); @@ -142,6 +178,8 @@ export function useNoteThreads( postId ) { return { notes, unresolvedNotes, + hasNotes: ( notesCount ?? 0 ) > 0, + hasUnresolvedNotes: ( unresolvedCount ?? 0 ) > 0, }; } diff --git a/packages/editor/src/components/collab-sidebar/index.js b/packages/editor/src/components/collab-sidebar/index.js index 39bcfd0a713a86..1ba9bb1f14cf68 100644 --- a/packages/editor/src/components/collab-sidebar/index.js +++ b/packages/editor/src/components/collab-sidebar/index.js @@ -67,15 +67,16 @@ function NotesSidebar( { postId } ) { [] ); - const { notes, unresolvedNotes } = useNoteThreads( postId ); + const { notes, unresolvedNotes, hasNotes, hasUnresolvedNotes } = + useNoteThreads( postId ); // Only enable the floating sidebar for large viewports. const showFloatingSidebar = isLargeViewport; // Fallback to "All notes" sidebar on smaller viewports. - const showAllNotesSidebar = notes.length > 0 || ! showFloatingSidebar; + const showAllNotesSidebar = hasNotes || ! showFloatingSidebar; useEnableFloatingSidebar( showFloatingSidebar && - ( unresolvedNotes.length > 0 || selectedNote !== undefined ) + ( hasUnresolvedNotes || selectedNote !== undefined ) ); async function focusNote( {