Skip to content

Commit 454ddbf

Browse files
lezamaenejb
authored andcommitted
Forms: Optimize counts with caching and shared state
1 parent 0df41cc commit 454ddbf

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
@@ -364,6 +364,22 @@ static function ( $post_id ) {
364364
* @param WP_REST_Request $request Full data about the request.
365365
* @return WP_REST_Response Response object on success.
366366
*/
367+
/**
368+
* Clears the cached status counts for the default view.
369+
* Called when feedback items are created, updated, or deleted.
370+
*/
371+
private function clear_status_counts_cache() {
372+
delete_transient( 'jetpack_forms_status_counts_default' );
373+
}
374+
375+
/**
376+
* Get status counts for feedback items.
377+
* Returns inbox, spam, and trash counts with optional filtering.
378+
* Only caches the default view (no filters) for performance.
379+
*
380+
* @param WP_REST_Request $request Full data about the request.
381+
* @return WP_REST_Response Response object on success.
382+
*/
367383
public function get_status_counts( $request ) {
368384
global $wpdb;
369385

@@ -372,10 +388,12 @@ public function get_status_counts( $request ) {
372388
$before = $request->get_param( 'before' );
373389
$after = $request->get_param( 'after' );
374390

375-
$cache_key = 'jetpack_forms_status_counts_' . md5( wp_json_encode( compact( 'search', 'parent', 'before', 'after' ) ) );
376-
$cached_result = get_transient( $cache_key );
377-
if ( false !== $cached_result ) {
378-
return rest_ensure_response( $cached_result );
391+
$is_default_view = empty( $search ) && empty( $parent ) && empty( $before ) && empty( $after );
392+
if ( $is_default_view ) {
393+
$cached_result = get_transient( 'jetpack_forms_status_counts_default' );
394+
if ( false !== $cached_result ) {
395+
return rest_ensure_response( $cached_result );
396+
}
379397
}
380398

381399
$where_conditions = array( $wpdb->prepare( 'post_type = %s', 'feedback' ) );
@@ -422,7 +440,9 @@ public function get_status_counts( $request ) {
422440
'trash' => (int) ( $counts['trash'] ?? 0 ),
423441
);
424442

425-
set_transient( $cache_key, $result, 30 );
443+
if ( $is_default_view ) {
444+
set_transient( 'jetpack_forms_status_counts_default', $result, 30 );
445+
}
426446

427447
return rest_ensure_response( $result );
428448
}
@@ -628,6 +648,21 @@ public function get_item_schema() {
628648
return $this->add_additional_fields_schema( $this->schema );
629649
}
630650

651+
/**
652+
* Deletes the item.
653+
* Overrides the parent method to clear cached counts when an item is deleted.
654+
*
655+
* @param WP_REST_Request $request Request object.
656+
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
657+
*/
658+
public function delete_item( $request ) {
659+
$result = parent::delete_item( $request );
660+
if ( ! is_wp_error( $result ) ) {
661+
$this->clear_status_counts_cache();
662+
}
663+
return $result;
664+
}
665+
631666
/**
632667
* Updates the item.
633668
* Overrides the parent method to resend the email when the item is updated from spam to publish.
@@ -653,6 +688,8 @@ public function update_item( $request ) {
653688
do_action( 'contact_form_akismet', 'ham', $akismet_values );
654689
$this->resend_email( $post_id );
655690
}
691+
// Clear cached counts when status changes
692+
$this->clear_status_counts_cache();
656693
}
657694
return $updated_item;
658695
}
@@ -874,6 +911,10 @@ public function delete_posts_by_status( $request ) { //phpcs:ignore VariableAnal
874911
++$deleted;
875912
}
876913

914+
if ( $deleted > 0 ) {
915+
$this->clear_status_counts_cache();
916+
}
917+
877918
return new WP_REST_Response( array( 'deleted' => $deleted ), 200 );
878919
}
879920

@@ -892,6 +933,7 @@ private function bulk_action_mark_as_spam( $post_ids ) {
892933
get_post_meta( $post_id, '_feedback_akismet_values', true )
893934
);
894935
}
936+
$this->clear_status_counts_cache();
895937
return new WP_REST_Response( array(), 200 );
896938
}
897939

@@ -910,6 +952,7 @@ private function bulk_action_mark_as_not_spam( $post_ids ) {
910952
get_post_meta( $post_id, '_feedback_akismet_values', true )
911953
);
912954
}
955+
$this->clear_status_counts_cache();
913956
return new WP_REST_Response( array(), 200 );
914957
}
915958

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

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

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

82-
const { selectedResponsesCount, currentStatus, currentQuery, filterOptions } = useSelect(
84+
const {
85+
selectedResponsesCount,
86+
currentStatus,
87+
currentQuery,
88+
filterOptions,
89+
totalItemsInbox,
90+
totalItemsSpam,
91+
totalItemsTrash,
92+
} = useSelect(
8393
select => ( {
8494
selectedResponsesCount: select( dashboardStore ).getSelectedResponsesCount(),
8595
currentStatus: select( dashboardStore ).getCurrentStatus(),
8696
currentQuery: select( dashboardStore ).getCurrentQuery(),
8797
filterOptions: select( dashboardStore ).getFilters(),
98+
totalItemsInbox: select( dashboardStore ).getInboxCount(),
99+
totalItemsSpam: select( dashboardStore ).getSpamCount(),
100+
totalItemsTrash: select( dashboardStore ).getTrashCount(),
88101
} ),
89102
[]
90103
);
@@ -115,7 +128,6 @@ export default function useInboxData(): UseInboxDataReturn {
115128
[ rawRecords ]
116129
);
117130

118-
const [ counts, setCounts ] = useState( { inbox: 0, spam: 0, trash: 0 } );
119131
const [ isLoadingCounts, setIsLoadingCounts ] = useState( false );
120132

121133
useEffect( () => {
@@ -143,12 +155,18 @@ export default function useInboxData(): UseInboxDataReturn {
143155
};
144156

145157
fetchCounts();
146-
}, [ currentQuery?.search, currentQuery?.parent, currentQuery?.before, currentQuery?.after ] );
158+
}, [
159+
currentQuery?.search,
160+
currentQuery?.parent,
161+
currentQuery?.before,
162+
currentQuery?.after,
163+
setCounts,
164+
] );
147165

148166
return {
149-
totalItemsInbox: counts.inbox,
150-
totalItemsSpam: counts.spam,
151-
totalItemsTrash: counts.trash,
167+
totalItemsInbox,
168+
totalItemsSpam,
169+
totalItemsTrash,
152170
records,
153171
isLoadingData: isLoadingRecordsData,
154172
isLoadingCounts,
@@ -161,5 +179,6 @@ export default function useInboxData(): UseInboxDataReturn {
161179
currentQuery,
162180
setCurrentQuery,
163181
filterOptions,
182+
updateCountsOptimistically,
164183
};
165184
}

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

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

125126
const queryArgs = useMemo( () => {
@@ -333,14 +334,36 @@ export default function InboxView() {
333334
);
334335

335336
const actions = useMemo( () => {
337+
// Wrap actions with optimistic updates
338+
const wrapActionWithOptimisticUpdate = ( action, toStatus ) => ( {
339+
...action,
340+
async callback( items, context ) {
341+
// statusFilter represents the current view: 'draft,publish' (inbox), 'spam', or 'trash'
342+
// For inbox, we need to map to individual status from items
343+
const fromStatus = statusFilter === 'draft,publish' ? items[ 0 ]?.status : statusFilter;
344+
// Optimistically update counts
345+
updateCountsOptimistically( fromStatus, toStatus, items.length );
346+
// Call original action
347+
return action.callback( items, context );
348+
},
349+
} );
350+
336351
const _actions = [
337352
markAsReadAction,
338353
markAsUnreadAction,
339-
markAsSpamAction,
340-
markAsNotSpamAction,
341-
moveToTrashAction,
342-
restoreAction,
343-
deleteAction,
354+
wrapActionWithOptimisticUpdate( markAsSpamAction, 'spam' ),
355+
wrapActionWithOptimisticUpdate( markAsNotSpamAction, 'publish' ),
356+
wrapActionWithOptimisticUpdate( moveToTrashAction, 'trash' ),
357+
wrapActionWithOptimisticUpdate( restoreAction, 'publish' ),
358+
{
359+
...deleteAction,
360+
async callback( items, context ) {
361+
const fromStatus = statusFilter === 'draft,publish' ? items[ 0 ]?.status : statusFilter;
362+
// Optimistically update counts (permanent delete, no toStatus)
363+
updateCountsOptimistically( fromStatus, 'deleted', items.length );
364+
return deleteAction.callback( items, context );
365+
},
366+
},
344367
];
345368
if ( isMobile ) {
346369
_actions.unshift( viewActionModal );
@@ -356,7 +379,7 @@ export default function InboxView() {
356379
} );
357380
}
358381
return _actions;
359-
}, [ isMobile, onChangeSelection, selection ] );
382+
}, [ isMobile, onChangeSelection, selection, updateCountsOptimistically, statusFilter ] );
360383

361384
const resetPage = useCallback( () => {
362385
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)