Skip to content

Commit 51096e2

Browse files
committed
Forms: Optimize counts with caching and shared state
1 parent d6ea2bf commit 51096e2

File tree

7 files changed

+136
-19
lines changed

7 files changed

+136
-19
lines changed

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

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,22 @@ static function ( $post_id ) {
341341
* @param WP_REST_Request $request Full data about the request.
342342
* @return WP_REST_Response Response object on success.
343343
*/
344+
/**
345+
* Clears the cached status counts for the default view.
346+
* Called when feedback items are created, updated, or deleted.
347+
*/
348+
private function clear_status_counts_cache() {
349+
delete_transient( 'jetpack_forms_status_counts_default' );
350+
}
351+
352+
/**
353+
* Get status counts for feedback items.
354+
* Returns inbox, spam, and trash counts with optional filtering.
355+
* Only caches the default view (no filters) for performance.
356+
*
357+
* @param WP_REST_Request $request Full data about the request.
358+
* @return WP_REST_Response Response object on success.
359+
*/
344360
public function get_status_counts( $request ) {
345361
global $wpdb;
346362

@@ -349,10 +365,12 @@ public function get_status_counts( $request ) {
349365
$before = $request->get_param( 'before' );
350366
$after = $request->get_param( 'after' );
351367

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 );
368+
$is_default_view = empty( $search ) && empty( $parent ) && empty( $before ) && empty( $after );
369+
if ( $is_default_view ) {
370+
$cached_result = get_transient( 'jetpack_forms_status_counts_default' );
371+
if ( false !== $cached_result ) {
372+
return rest_ensure_response( $cached_result );
373+
}
356374
}
357375

358376
$where_conditions = array( $wpdb->prepare( 'post_type = %s', 'feedback' ) );
@@ -399,7 +417,9 @@ public function get_status_counts( $request ) {
399417
'trash' => (int) ( $counts['trash'] ?? 0 ),
400418
);
401419

402-
set_transient( $cache_key, $result, 30 );
420+
if ( $is_default_view ) {
421+
set_transient( 'jetpack_forms_status_counts_default', $result, 30 );
422+
}
403423

404424
return rest_ensure_response( $result );
405425
}
@@ -595,6 +615,21 @@ public function get_item_schema() {
595615
return $this->add_additional_fields_schema( $this->schema );
596616
}
597617

618+
/**
619+
* Deletes the item.
620+
* Overrides the parent method to clear cached counts when an item is deleted.
621+
*
622+
* @param WP_REST_Request $request Request object.
623+
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
624+
*/
625+
public function delete_item( $request ) {
626+
$result = parent::delete_item( $request );
627+
if ( ! is_wp_error( $result ) ) {
628+
$this->clear_status_counts_cache();
629+
}
630+
return $result;
631+
}
632+
598633
/**
599634
* Updates the item.
600635
* Overrides the parent method to resend the email when the item is updated from spam to publish.
@@ -620,6 +655,8 @@ public function update_item( $request ) {
620655
do_action( 'contact_form_akismet', 'ham', $akismet_values );
621656
$this->resend_email( $post_id );
622657
}
658+
// Clear cached counts when status changes
659+
$this->clear_status_counts_cache();
623660
}
624661
return $updated_item;
625662
}
@@ -837,6 +874,10 @@ public function delete_posts_by_status( $request ) { //phpcs:ignore VariableAnal
837874
++$deleted;
838875
}
839876

877+
if ( $deleted > 0 ) {
878+
$this->clear_status_counts_cache();
879+
}
880+
840881
return new WP_REST_Response( array( 'deleted' => $deleted ), 200 );
841882
}
842883

@@ -855,6 +896,7 @@ private function bulk_action_mark_as_spam( $post_ids ) {
855896
get_post_meta( $post_id, '_feedback_akismet_values', true )
856897
);
857898
}
899+
$this->clear_status_counts_cache();
858900
return new WP_REST_Response( array(), 200 );
859901
}
860902

@@ -873,6 +915,7 @@ private function bulk_action_mark_as_not_spam( $post_ids ) {
873915
get_post_meta( $post_id, '_feedback_akismet_values', true )
874916
);
875917
}
918+
$this->clear_status_counts_cache();
876919
return new WP_REST_Response( array(), 200 );
877920
}
878921

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ interface UseInboxDataReturn {
4949
currentQuery: Record< string, unknown >;
5050
setCurrentQuery: ( query: Record< string, unknown > ) => void;
5151
filterOptions: Record< string, unknown >;
52+
updateCountsOptimistically: ( fromStatus: string, toStatus: string, count?: number ) => void;
5253
}
5354

5455
const RESPONSE_FIELDS = [
@@ -74,16 +75,28 @@ const RESPONSE_FIELDS = [
7475
*/
7576
export default function useInboxData(): UseInboxDataReturn {
7677
const [ searchParams ] = useSearchParams();
77-
const { setCurrentQuery, setSelectedResponses } = useDispatch( dashboardStore );
78+
const { setCurrentQuery, setSelectedResponses, setCounts, updateCountsOptimistically } =
79+
useDispatch( dashboardStore );
7880
const urlStatus = searchParams.get( 'status' );
7981
const statusFilter = getStatusFilter( urlStatus );
8082

81-
const { selectedResponsesCount, currentStatus, currentQuery, filterOptions } = useSelect(
83+
const {
84+
selectedResponsesCount,
85+
currentStatus,
86+
currentQuery,
87+
filterOptions,
88+
totalItemsInbox,
89+
totalItemsSpam,
90+
totalItemsTrash,
91+
} = useSelect(
8292
select => ( {
8393
selectedResponsesCount: select( dashboardStore ).getSelectedResponsesCount(),
8494
currentStatus: select( dashboardStore ).getCurrentStatus(),
8595
currentQuery: select( dashboardStore ).getCurrentQuery(),
8696
filterOptions: select( dashboardStore ).getFilters(),
97+
totalItemsInbox: select( dashboardStore ).getInboxCount(),
98+
totalItemsSpam: select( dashboardStore ).getSpamCount(),
99+
totalItemsTrash: select( dashboardStore ).getTrashCount(),
87100
} ),
88101
[]
89102
);
@@ -100,7 +113,6 @@ export default function useInboxData(): UseInboxDataReturn {
100113

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

103-
const [ counts, setCounts ] = useState( { inbox: 0, spam: 0, trash: 0 } );
104116
const [ isLoadingCounts, setIsLoadingCounts ] = useState( false );
105117

106118
useEffect( () => {
@@ -128,12 +140,18 @@ export default function useInboxData(): UseInboxDataReturn {
128140
};
129141

130142
fetchCounts();
131-
}, [ currentQuery?.search, currentQuery?.parent, currentQuery?.before, currentQuery?.after ] );
143+
}, [
144+
currentQuery?.search,
145+
currentQuery?.parent,
146+
currentQuery?.before,
147+
currentQuery?.after,
148+
setCounts,
149+
] );
132150

133151
return {
134-
totalItemsInbox: counts.inbox,
135-
totalItemsSpam: counts.spam,
136-
totalItemsTrash: counts.trash,
152+
totalItemsInbox,
153+
totalItemsSpam,
154+
totalItemsTrash,
137155
records,
138156
isLoadingData: isLoadingRecordsData,
139157
isLoadingCounts,
@@ -146,5 +164,6 @@ export default function useInboxData(): UseInboxDataReturn {
146164
currentQuery,
147165
setCurrentQuery,
148166
filterOptions,
167+
updateCountsOptimistically,
149168
};
150169
}

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export default function InboxView() {
118118
isLoadingData,
119119
totalItems,
120120
totalPages,
121+
updateCountsOptimistically,
121122
} = useInboxData();
122123

123124
const queryArgs = useMemo( () => {
@@ -301,12 +302,34 @@ export default function InboxView() {
301302
);
302303

303304
const actions = useMemo( () => {
305+
// Wrap actions with optimistic updates
306+
const wrapActionWithOptimisticUpdate = ( action, toStatus ) => ( {
307+
...action,
308+
async callback( items, context ) {
309+
// statusFilter represents the current view: 'draft,publish' (inbox), 'spam', or 'trash'
310+
// For inbox, we need to map to individual status from items
311+
const fromStatus = statusFilter === 'draft,publish' ? items[ 0 ]?.status : statusFilter;
312+
// Optimistically update counts
313+
updateCountsOptimistically( fromStatus, toStatus, items.length );
314+
// Call original action
315+
return action.callback( items, context );
316+
},
317+
} );
318+
304319
const _actions = [
305-
markAsSpamAction,
306-
markAsNotSpamAction,
307-
moveToTrashAction,
308-
restoreAction,
309-
deleteAction,
320+
wrapActionWithOptimisticUpdate( markAsSpamAction, 'spam' ),
321+
wrapActionWithOptimisticUpdate( markAsNotSpamAction, 'publish' ),
322+
wrapActionWithOptimisticUpdate( moveToTrashAction, 'trash' ),
323+
wrapActionWithOptimisticUpdate( restoreAction, 'publish' ),
324+
{
325+
...deleteAction,
326+
async callback( items, context ) {
327+
const fromStatus = statusFilter === 'draft,publish' ? items[ 0 ]?.status : statusFilter;
328+
// Optimistically update counts (permanent delete, no toStatus)
329+
updateCountsOptimistically( fromStatus, 'deleted', items.length );
330+
return deleteAction.callback( items, context );
331+
},
332+
},
310333
];
311334
if ( isMobile ) {
312335
_actions.unshift( viewActionModal );
@@ -322,7 +345,7 @@ export default function InboxView() {
322345
} );
323346
}
324347
return _actions;
325-
}, [ isMobile, onChangeSelection, selection ] );
348+
}, [ isMobile, onChangeSelection, selection, updateCountsOptimistically, statusFilter ] );
326349

327350
const resetPage = useCallback( () => {
328351
view.page = 1;

projects/packages/forms/src/dashboard/store/action-types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export const RECEIVE_FILTERS = 'RECEIVE_FILTERS';
22
export const INVALIDATE_FILTERS = 'INVALIDATE_FILTERS';
33
export const SET_CURRENT_QUERY = 'SET_CURRENT_QUERY';
44
export const SET_SELECTED_RESPONSES = 'SET_SELECTED_RESPONSES';
5+
export const SET_COUNTS = 'SET_COUNTS';

projects/packages/forms/src/dashboard/store/actions.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
RECEIVE_FILTERS,
88
SET_CURRENT_QUERY,
99
INVALIDATE_FILTERS,
10+
SET_COUNTS,
1011
} from './action-types';
1112

1213
/**
@@ -52,6 +53,19 @@ export function setCurrentQuery( currentQuery ) {
5253
};
5354
}
5455

56+
/**
57+
* Set the status counts.
58+
*
59+
* @param {object} counts - The counts object with inbox, spam, and trash.
60+
* @return {object} Action object.
61+
*/
62+
export function setCounts( counts ) {
63+
return {
64+
type: SET_COUNTS,
65+
counts,
66+
};
67+
}
68+
5569
/**
5670
* Performs a bulk action on responses.
5771
*

projects/packages/forms/src/dashboard/store/reducer.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { isEqual } from 'lodash';
66
/**
77
* Internal dependencies
88
*/
9-
import { SET_SELECTED_RESPONSES, RECEIVE_FILTERS, SET_CURRENT_QUERY } from './action-types';
9+
import {
10+
SET_SELECTED_RESPONSES,
11+
RECEIVE_FILTERS,
12+
SET_CURRENT_QUERY,
13+
SET_COUNTS,
14+
} from './action-types';
1015

1116
const filters = ( state = {}, action ) => {
1217
if ( action.type === RECEIVE_FILTERS ) {
@@ -29,8 +34,16 @@ const selectedResponsesFromCurrentDataset = ( state = [], action ) => {
2934
return state;
3035
};
3136

37+
const counts = ( state = { inbox: 0, spam: 0, trash: 0 }, action ) => {
38+
if ( action.type === SET_COUNTS ) {
39+
return action.counts;
40+
}
41+
return state;
42+
};
43+
3244
export default combineReducers( {
3345
selectedResponsesFromCurrentDataset,
3446
filters,
3547
currentQuery,
48+
counts,
3649
} );

projects/packages/forms/src/dashboard/store/selectors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ export const getCurrentStatus = state => state.currentQuery?.status ?? 'draft,pu
44
export const getSelectedResponsesFromCurrentDataset = state =>
55
state.selectedResponsesFromCurrentDataset;
66
export const getSelectedResponsesCount = state => state.selectedResponsesFromCurrentDataset.length;
7+
export const getCounts = state => state.counts;
8+
export const getInboxCount = state => state.counts.inbox;
9+
export const getSpamCount = state => state.counts.spam;
10+
export const getTrashCount = state => state.counts.trash;

0 commit comments

Comments
 (0)