Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions projects/packages/forms/changelog/optimize-inbox-counts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Forms: replace 3 separate count queries with single optimized counts endpoint.
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,39 @@ public function register_routes() {
'callback' => array( $this, 'get_forms_config' ),
)
);

register_rest_route(
$this->namespace,
$this->rest_base . '/counts',
array(
'methods' => \WP_REST_Server::READABLE,
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'callback' => array( $this, 'get_status_counts' ),
'args' => array(
'search' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'parent' => array(
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => function ( $value ) {
return array_map( 'absint', (array) $value );
},
),
'before' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'after' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
}

/**
Expand Down Expand Up @@ -302,6 +335,75 @@ static function ( $post_id ) {
);
}

/**
* Retrieves status counts for inbox, spam, and trash in a single optimized query.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response Response object on success.
*/
public function get_status_counts( $request ) {
global $wpdb;

$search = $request->get_param( 'search' );
$parent = $request->get_param( 'parent' );
$before = $request->get_param( 'before' );
$after = $request->get_param( 'after' );

$cache_key = 'jetpack_forms_status_counts_' . md5( wp_json_encode( compact( 'search', 'parent', 'before', 'after' ) ) );
$cached_result = get_transient( $cache_key );
if ( false !== $cached_result ) {
return rest_ensure_response( $cached_result );
}

$where_conditions = array( $wpdb->prepare( 'post_type = %s', 'feedback' ) );
$join_clauses = '';

if ( ! empty( $search ) ) {
$search_like = '%' . $wpdb->esc_like( $search ) . '%';
$where_conditions[] = $wpdb->prepare( '(post_title LIKE %s OR post_content LIKE %s)', $search_like, $search_like );
}

if ( ! empty( $parent ) && is_array( $parent ) ) {
$parent_ids = array_map( 'absint', $parent );
$parent_ids_string = implode( ',', $parent_ids );
$where_conditions[] = "post_parent IN ($parent_ids_string)";
}

if ( ! empty( $before ) || ! empty( $after ) ) {
if ( ! empty( $before ) ) {
$where_conditions[] = $wpdb->prepare( 'post_date <= %s', $before );
}
if ( ! empty( $after ) ) {
$where_conditions[] = $wpdb->prepare( 'post_date >= %s', $after );
}
}

$where_clause = implode( ' AND ', $where_conditions );

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$counts = $wpdb->get_row(
"SELECT
SUM(CASE WHEN post_status IN ('publish', 'draft') THEN 1 ELSE 0 END) as inbox,
SUM(CASE WHEN post_status = 'spam' THEN 1 ELSE 0 END) as spam,
SUM(CASE WHEN post_status = 'trash' THEN 1 ELSE 0 END) as trash
FROM $wpdb->posts
$join_clauses
WHERE $where_clause",
ARRAY_A
);
// phpcs:enable

$result = array(
'inbox' => (int) ( $counts['inbox'] ?? 0 ),
'spam' => (int) ( $counts['spam'] ?? 0 ),
'trash' => (int) ( $counts['trash'] ?? 0 ),
);

set_transient( $cache_key, $result, 30 );

return rest_ensure_response( $result );
}

/**
* Adds the additional fields to the item's schema.
*
Expand Down
77 changes: 36 additions & 41 deletions projects/packages/forms/src/dashboard/hooks/use-inbox-data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { useEntityRecords } from '@wordpress/core-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url';
import { useSearchParams } from 'react-router';
/**
* Internal dependencies
Expand Down Expand Up @@ -36,6 +39,7 @@ interface UseInboxDataReturn {
totalItemsTrash: number;
records: FormResponse[];
isLoadingData: boolean;
isLoadingCounts: boolean;
totalItems: number;
totalPages: number;
selectedResponsesCount: number;
Expand Down Expand Up @@ -77,52 +81,43 @@ export default function useInboxData(): UseInboxDataReturn {

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

const { isResolving: isLoadingInboxData, totalItems: totalItemsInbox = 0 } = useEntityRecords(
'postType',
'feedback',
{
page: 1,
search: '',
...currentQuery,
status: 'publish,draft',
per_page: 1,
_fields: 'id',
}
);
const [ counts, setCounts ] = useState( { inbox: 0, spam: 0, trash: 0 } );
const [ isLoadingCounts, setIsLoadingCounts ] = useState( false );

const { isResolving: isLoadingSpamData, totalItems: totalItemsSpam = 0 } = useEntityRecords(
'postType',
'feedback',
{
page: 1,
search: '',
...currentQuery,
status: 'spam',
per_page: 1,
_fields: 'id',
}
);
useEffect( () => {
const fetchCounts = async () => {
setIsLoadingCounts( true );
const params: Record< string, unknown > = {};
if ( currentQuery?.search ) {
params.search = currentQuery.search;
}
if ( currentQuery?.parent ) {
params.parent = currentQuery.parent;
}
if ( currentQuery?.before ) {
params.before = currentQuery.before;
}
if ( currentQuery?.after ) {
params.after = currentQuery.after;
}
const path = addQueryArgs( '/wp/v2/feedback/counts', params );
const response = await apiFetch< { inbox: number; spam: number; trash: number } >( {
path,
} );
setCounts( response );
setIsLoadingCounts( false );
};

const { isResolving: isLoadingTrashData, totalItems: totalItemsTrash = 0 } = useEntityRecords(
'postType',
'feedback',
{
page: 1,
search: '',
...currentQuery,
status: 'trash',
per_page: 1,
_fields: 'id',
}
);
fetchCounts();
}, [ currentQuery ] );

return {
totalItemsInbox,
totalItemsSpam,
totalItemsTrash,
totalItemsInbox: counts.inbox,
totalItemsSpam: counts.spam,
totalItemsTrash: counts.trash,
records,
isLoadingData:
isLoadingRecordsData || isLoadingInboxData || isLoadingSpamData || isLoadingTrashData,
isLoadingData: isLoadingRecordsData,
isLoadingCounts,
totalItems,
totalPages,
selectedResponsesCount,
Expand Down
13 changes: 7 additions & 6 deletions projects/packages/forms/src/dashboard/inbox/dataviews/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function InboxView() {
totalPages,
} = useInboxData();

useEffect( () => {
const queryArgs = useMemo( () => {
const _filters = view.filters?.reduce( ( accumulator, { field, value } ) => {
if ( ! value ) {
return accumulator;
Expand All @@ -135,17 +135,18 @@ export default function InboxView() {
}
return accumulator;
}, {} );
const _queryArgs = {
return {
per_page: view.perPage,
page: view.page,
search: view.search,
..._filters,
status: statusFilter,
};
// We need to keep the current query args in the store to be used in `export`
// and for getting the total records per `status`.
setCurrentQuery( _queryArgs );
}, [ view, statusFilter, setCurrentQuery ] );
}, [ view.perPage, view.page, view.search, view.filters, statusFilter ] );

useEffect( () => {
setCurrentQuery( queryArgs );
}, [ queryArgs, setCurrentQuery ] );
const data = useMemo(
() =>
records?.map( record => ( {
Expand Down
3 changes: 2 additions & 1 deletion projects/packages/forms/src/dashboard/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import { combineReducers } from '@wordpress/data';
import { isEqual } from 'lodash';
/**
* Internal dependencies
*/
Expand All @@ -16,7 +17,7 @@ const filters = ( state = {}, action ) => {

const currentQuery = ( state = {}, action ) => {
if ( action.type === SET_CURRENT_QUERY ) {
return action.currentQuery;
return isEqual( state, action.currentQuery ) ? state : action.currentQuery;
}
return state;
};
Expand Down
Loading