Skip to content

Commit b6171a4

Browse files
committed
Forms: Replace 3 count queries with single optimized endpoint
Reduces from 3 separate REST requests to 1 optimized database query with caching. Related: #45339
1 parent 845fb3a commit b6171a4

File tree

4 files changed

+149
-47
lines changed

4 files changed

+149
-47
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
4+
Forms: replace 3 separate count queries with single optimized counts endpoint.

projects/packages/forms/src/contact-form/class-contact-form-endpoint.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,39 @@ public function register_routes() {
254254
'callback' => array( $this, 'get_forms_config' ),
255255
)
256256
);
257+
258+
register_rest_route(
259+
$this->namespace,
260+
$this->rest_base . '/counts',
261+
array(
262+
'methods' => \WP_REST_Server::READABLE,
263+
'permission_callback' => array( $this, 'get_items_permissions_check' ),
264+
'callback' => array( $this, 'get_status_counts' ),
265+
'args' => array(
266+
'search' => array(
267+
'type' => 'string',
268+
'sanitize_callback' => 'sanitize_text_field',
269+
),
270+
'parent' => array(
271+
'type' => 'array',
272+
'items' => array(
273+
'type' => 'integer',
274+
),
275+
'sanitize_callback' => function ( $value ) {
276+
return array_map( 'absint', (array) $value );
277+
},
278+
),
279+
'before' => array(
280+
'type' => 'string',
281+
'sanitize_callback' => 'sanitize_text_field',
282+
),
283+
'after' => array(
284+
'type' => 'string',
285+
'sanitize_callback' => 'sanitize_text_field',
286+
),
287+
),
288+
)
289+
);
257290
}
258291

259292
/**
@@ -302,6 +335,75 @@ static function ( $post_id ) {
302335
);
303336
}
304337

338+
/**
339+
* Retrieves status counts for inbox, spam, and trash in a single optimized query.
340+
*
341+
* @param WP_REST_Request $request Full data about the request.
342+
* @return WP_REST_Response Response object on success.
343+
*/
344+
public function get_status_counts( $request ) {
345+
global $wpdb;
346+
347+
$search = $request->get_param( 'search' );
348+
$parent = $request->get_param( 'parent' );
349+
$before = $request->get_param( 'before' );
350+
$after = $request->get_param( 'after' );
351+
352+
$cache_key = 'jetpack_forms_status_counts_' . md5( wp_json_encode( compact( 'search', 'parent', 'before', 'after' ) ) );
353+
$cached_result = get_transient( $cache_key );
354+
if ( false !== $cached_result ) {
355+
return rest_ensure_response( $cached_result );
356+
}
357+
358+
$where_conditions = array( $wpdb->prepare( 'post_type = %s', 'feedback' ) );
359+
$join_clauses = '';
360+
361+
if ( ! empty( $search ) ) {
362+
$search_like = '%' . $wpdb->esc_like( $search ) . '%';
363+
$where_conditions[] = $wpdb->prepare( '(post_title LIKE %s OR post_content LIKE %s)', $search_like, $search_like );
364+
}
365+
366+
if ( ! empty( $parent ) && is_array( $parent ) ) {
367+
$parent_ids = array_map( 'absint', $parent );
368+
$parent_ids_string = implode( ',', $parent_ids );
369+
$where_conditions[] = "post_parent IN ($parent_ids_string)";
370+
}
371+
372+
if ( ! empty( $before ) || ! empty( $after ) ) {
373+
if ( ! empty( $before ) ) {
374+
$where_conditions[] = $wpdb->prepare( 'post_date <= %s', $before );
375+
}
376+
if ( ! empty( $after ) ) {
377+
$where_conditions[] = $wpdb->prepare( 'post_date >= %s', $after );
378+
}
379+
}
380+
381+
$where_clause = implode( ' AND ', $where_conditions );
382+
383+
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
384+
$counts = $wpdb->get_row(
385+
"SELECT
386+
SUM(CASE WHEN post_status IN ('publish', 'draft') THEN 1 ELSE 0 END) as inbox,
387+
SUM(CASE WHEN post_status = 'spam' THEN 1 ELSE 0 END) as spam,
388+
SUM(CASE WHEN post_status = 'trash' THEN 1 ELSE 0 END) as trash
389+
FROM $wpdb->posts
390+
$join_clauses
391+
WHERE $where_clause",
392+
ARRAY_A
393+
);
394+
// phpcs:enable
395+
396+
$result = array(
397+
'inbox' => (int) ( $counts['inbox'] ?? 0 ),
398+
'spam' => (int) ( $counts['spam'] ?? 0 ),
399+
'trash' => (int) ( $counts['trash'] ?? 0 ),
400+
);
401+
402+
set_transient( $cache_key, $result, 30 );
403+
404+
return rest_ensure_response( $result );
405+
}
406+
305407
/**
306408
* Adds the additional fields to the item's schema.
307409
*

projects/packages/forms/src/dashboard/hooks/use-inbox-data.ts

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/**
22
* External dependencies
33
*/
4+
import apiFetch from '@wordpress/api-fetch';
45
import { useEntityRecords } from '@wordpress/core-data';
56
import { useDispatch, useSelect } from '@wordpress/data';
7+
import { useEffect, useState } from '@wordpress/element';
8+
import { addQueryArgs } from '@wordpress/url';
69
import { useSearchParams } from 'react-router';
710
/**
811
* Internal dependencies
@@ -36,6 +39,7 @@ interface UseInboxDataReturn {
3639
totalItemsTrash: number;
3740
records: FormResponse[];
3841
isLoadingData: boolean;
42+
isLoadingCounts: boolean;
3943
totalItems: number;
4044
totalPages: number;
4145
selectedResponsesCount: number;
@@ -77,52 +81,43 @@ export default function useInboxData(): UseInboxDataReturn {
7781

7882
const records = ( rawRecords || [] ) as FormResponse[];
7983

80-
const { isResolving: isLoadingInboxData, totalItems: totalItemsInbox = 0 } = useEntityRecords(
81-
'postType',
82-
'feedback',
83-
{
84-
page: 1,
85-
search: '',
86-
...currentQuery,
87-
status: 'publish,draft',
88-
per_page: 1,
89-
_fields: 'id',
90-
}
91-
);
84+
const [ counts, setCounts ] = useState( { inbox: 0, spam: 0, trash: 0 } );
85+
const [ isLoadingCounts, setIsLoadingCounts ] = useState( false );
9286

93-
const { isResolving: isLoadingSpamData, totalItems: totalItemsSpam = 0 } = useEntityRecords(
94-
'postType',
95-
'feedback',
96-
{
97-
page: 1,
98-
search: '',
99-
...currentQuery,
100-
status: 'spam',
101-
per_page: 1,
102-
_fields: 'id',
103-
}
104-
);
87+
useEffect( () => {
88+
const fetchCounts = async () => {
89+
setIsLoadingCounts( true );
90+
const params: Record< string, unknown > = {};
91+
if ( currentQuery?.search ) {
92+
params.search = currentQuery.search;
93+
}
94+
if ( currentQuery?.parent ) {
95+
params.parent = currentQuery.parent;
96+
}
97+
if ( currentQuery?.before ) {
98+
params.before = currentQuery.before;
99+
}
100+
if ( currentQuery?.after ) {
101+
params.after = currentQuery.after;
102+
}
103+
const path = addQueryArgs( '/wp/v2/feedback/counts', params );
104+
const response = await apiFetch< { inbox: number; spam: number; trash: number } >( {
105+
path,
106+
} );
107+
setCounts( response );
108+
setIsLoadingCounts( false );
109+
};
105110

106-
const { isResolving: isLoadingTrashData, totalItems: totalItemsTrash = 0 } = useEntityRecords(
107-
'postType',
108-
'feedback',
109-
{
110-
page: 1,
111-
search: '',
112-
...currentQuery,
113-
status: 'trash',
114-
per_page: 1,
115-
_fields: 'id',
116-
}
117-
);
111+
fetchCounts();
112+
}, [ currentQuery ] );
118113

119114
return {
120-
totalItemsInbox,
121-
totalItemsSpam,
122-
totalItemsTrash,
115+
totalItemsInbox: counts.inbox,
116+
totalItemsSpam: counts.spam,
117+
totalItemsTrash: counts.trash,
123118
records,
124-
isLoadingData:
125-
isLoadingRecordsData || isLoadingInboxData || isLoadingSpamData || isLoadingTrashData,
119+
isLoadingData: isLoadingRecordsData,
120+
isLoadingCounts,
126121
totalItems,
127122
totalPages,
128123
selectedResponsesCount,

projects/packages/forms/src/dashboard/inbox/dataviews/index.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export default function InboxView() {
120120
totalPages,
121121
} = useInboxData();
122122

123-
useEffect( () => {
123+
const queryArgs = useMemo( () => {
124124
const _filters = view.filters?.reduce( ( accumulator, { field, value } ) => {
125125
if ( ! value ) {
126126
return accumulator;
@@ -135,17 +135,18 @@ export default function InboxView() {
135135
}
136136
return accumulator;
137137
}, {} );
138-
const _queryArgs = {
138+
return {
139139
per_page: view.perPage,
140140
page: view.page,
141141
search: view.search,
142142
..._filters,
143143
status: statusFilter,
144144
};
145-
// We need to keep the current query args in the store to be used in `export`
146-
// and for getting the total records per `status`.
147-
setCurrentQuery( _queryArgs );
148-
}, [ view, statusFilter, setCurrentQuery ] );
145+
}, [ view.perPage, view.page, view.search, view.filters, statusFilter ] );
146+
147+
useEffect( () => {
148+
setCurrentQuery( queryArgs );
149+
}, [ queryArgs, setCurrentQuery ] );
149150
const data = useMemo(
150151
() =>
151152
records?.map( record => ( {

0 commit comments

Comments
 (0)