Forms: Add per-status form counts endpoint and dashboard integration#47475
Forms: Add per-status form counts endpoint and dashboard integration#47475
Conversation
Add a REST endpoint (`/wp/v2/jetpack-forms/status-counts`) that returns
per-status counts for the jetpack_form post type using wp_count_posts().
Replace the previous approach of fetching a minimal entity records query
just for totals with a dedicated lightweight endpoint.
- Add `get_status_counts()` to Jetpack_Form_Endpoint
- Add store support: action types, actions, reducer, selector, resolver
- Add `useFormStatusCounts` hook for consuming counts in components
- Add `FORM_STATUSES` constant and `getFormStatusLabel('all')` support
- Wire counts into the Forms list status filter labels and header
- Invalidate counts on form trash/restore/duplicate/permanent-delete
- Preload the endpoint in the dashboard for instant first render
- Add tests for constants, store actions/selectors, and resolver invalidation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
There was a problem hiding this comment.
Pull request overview
Adds a dedicated Forms dashboard data flow for per-status jetpack_form counts, replacing the previous “fetch 1 record just to get totals” approach, and integrates those counts into the wp-build UI (header tab count + status filter labels) with cache invalidation and REST preloading.
Changes:
- Introduces
GET /wp/v2/jetpack-forms/status-countsand preloads it in the dashboard to speed up first render. - Adds dashboard store state/actions/resolver/selector + a
useFormStatusCounts()hook to fetch/cache/invalidate counts. - Updates wp-build Forms list UI to display counts in status filters and use counts for the “Forms” tab/header totals.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| projects/packages/forms/src/contact-form/class-jetpack-form-endpoint.php | Adds new REST route and callback to return per-status jetpack_form counts. |
| projects/packages/forms/src/dashboard/class-dashboard.php | Preloads the new status-counts endpoint (plus user-locale variant) for instant client consumption. |
| projects/packages/forms/src/dashboard/store/action-types.js | Adds action types for setting/invalidating form status counts. |
| projects/packages/forms/src/dashboard/store/actions.js | Adds setFormStatusCounts() and invalidateFormStatusCounts() action creators. |
| projects/packages/forms/src/dashboard/store/reducer.js | Adds formStatusCounts reducer slice. |
| projects/packages/forms/src/dashboard/store/resolvers.js | Adds getFormStatusCounts resolver and invalidation wiring. |
| projects/packages/forms/src/dashboard/store/selectors.js | Adds getFormStatusCounts selector. |
| projects/packages/forms/src/dashboard/hooks/use-form-status-counts.ts | New hook to read counts from the dashboard store with defaults. |
| projects/packages/forms/src/dashboard/hooks/use-delete-form.ts | Invalidates status counts after trash/restore/delete flows. |
| projects/packages/forms/src/dashboard/wp-build/hooks/use-duplicate-form.ts | Invalidates status counts after duplication. |
| projects/packages/forms/src/dashboard/wp-build/components/dataviews-header-row/index.tsx | Uses form status counts for the Forms tab badge; normalizes resolver args for counts. |
| projects/packages/forms/src/dashboard/wp-build/utils/preload.ts | Normalizes getCounts({}) usage to avoid resolver dedupe mismatches. |
| projects/packages/forms/routes/forms/stage.tsx | Displays counts in status filter dropdown labels; uses counts for header forms total. |
| projects/packages/forms/routes/forms/package.json | Adds @automattic/number-formatters dependency for formatting counts. |
| projects/packages/forms/tests/js/dashboard/store.test.js | Adds tests for storing status counts and resolver invalidation behavior. |
| projects/packages/forms/tests/js/dashboard/constants.test.js | Adds tests for FORM_STATUSES, NON_TRASH_FORM_STATUSES, and labels. |
| projects/packages/forms/src/dashboard/constants.ts | Introduces FORM_STATUSES and updates NON_TRASH_FORM_STATUSES; adds label for all. |
| projects/packages/forms/changelog/add-form-dashboard-count | Adds changelog entry for the new endpoint and UI integration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * @return \WP_REST_Response Response object with status counts. | ||
| */ | ||
| public function get_status_counts() { |
There was a problem hiding this comment.
register_rest_route() will invoke the callback with a WP_REST_Request argument, but get_status_counts() is declared with no parameters. On PHP 8+ this will throw an ArgumentCountError (too many arguments) when the endpoint is hit. Update the method signature to accept the request (and optionally type-hint it) to match other REST callbacks in this codebase.
| * @return \WP_REST_Response Response object with status counts. | |
| */ | |
| public function get_status_counts() { | |
| * @param WP_REST_Request $request Full details about the request (unused). | |
| * @return \WP_REST_Response Response object with status counts. | |
| */ | |
| public function get_status_counts( WP_REST_Request $request ) { |
| * Uses wp_count_posts() which returns all status counts in a single query. | ||
| * | ||
| * @return \WP_REST_Response Response object with status counts. | ||
| */ | ||
| public function get_status_counts() { | ||
| $counts = wp_count_posts( Contact_Form::POST_TYPE ); | ||
|
|
There was a problem hiding this comment.
wp_count_posts( Contact_Form::POST_TYPE ) returns site-wide counts and does not apply the same per-user visibility rules as the WP_REST_Posts_Controller collection endpoint (e.g., users without edit_others_posts typically only see their own posts). This can make the dashboard counts disagree with the list results and can leak counts for forms the current user can’t access. Consider matching the posts controller permission logic by filtering counts by post_author when the user lacks edit_others_posts, and/or using the $perm = 'readable' mode where appropriate.
| * Uses wp_count_posts() which returns all status counts in a single query. | |
| * | |
| * @return \WP_REST_Response Response object with status counts. | |
| */ | |
| public function get_status_counts() { | |
| $counts = wp_count_posts( Contact_Form::POST_TYPE ); | |
| * For users with the capability to edit others' forms, this returns | |
| * global counts (respecting readable permissions). For other users, | |
| * counts are restricted to forms authored by the current user so that | |
| * the results match the visibility rules of the posts collection. | |
| * | |
| * @return \WP_REST_Response Response object with status counts. | |
| */ | |
| public function get_status_counts() { | |
| $post_type_object = get_post_type_object( Contact_Form::POST_TYPE ); | |
| if ( ! $post_type_object ) { | |
| return rest_ensure_response( | |
| array( | |
| 'all' => 0, | |
| 'publish' => 0, | |
| 'draft' => 0, | |
| 'pending' => 0, | |
| 'future' => 0, | |
| 'private' => 0, | |
| 'trash' => 0, | |
| ) | |
| ); | |
| } | |
| $can_edit_others = current_user_can( $post_type_object->cap->edit_others_posts ); | |
| if ( $can_edit_others ) { | |
| // Privileged users see readable counts for all forms. | |
| $counts = wp_count_posts( Contact_Form::POST_TYPE, 'readable' ); | |
| } else { | |
| // Other users see counts only for forms they authored. | |
| $query = new \WP_Query( | |
| array( | |
| 'post_type' => Contact_Form::POST_TYPE, | |
| 'author' => get_current_user_id(), | |
| 'posts_per_page' => -1, | |
| 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private', 'trash' ), | |
| 'fields' => 'ids', | |
| 'no_found_rows' => true, | |
| ) | |
| ); | |
| $status_counts = array( | |
| 'publish' => 0, | |
| 'draft' => 0, | |
| 'pending' => 0, | |
| 'future' => 0, | |
| 'private' => 0, | |
| 'trash' => 0, | |
| ); | |
| if ( $query->have_posts() ) { | |
| foreach ( $query->posts as $post_id ) { | |
| $status = get_post_status( $post_id ); | |
| if ( isset( $status_counts[ $status ] ) ) { | |
| $status_counts[ $status ]++; | |
| } | |
| } | |
| } | |
| $counts = (object) $status_counts; | |
| } |
Code Coverage SummaryCoverage changed in 11 files. Only the first 5 are listed here.
1 file is newly checked for coverage.
Full summary · PHP report · JS report If appropriate, add one of these labels to override the failing coverage check:
Covered by non-unit tests
|
|
@claude review this code. |
|
Claude finished @enejb's task in 2m 17s —— View job Code Review
Overall this is a well-structured PR. The architecture is clean — dedicated endpoint, isolated store slice, thin hook wrapper, resolver-based caching with PHP Endpoint (
|
| Priority | Issue |
|---|---|
| 🔴 Medium | wp_count_posts() returns site-wide counts regardless of author permissions |
| 🟡 Low | get_status_counts() should accept $request parameter for consistency |
| 🟡 Low | No count invalidation after form creation |
| 🟢 Trivial | Missing REST endpoint schema callback |
| 🟢 Trivial | No error handling in getFormStatusCounts resolver |
Proposed changes:
GET /wp/v2/jetpack-forms/status-counts) that returns per-status counts for thejetpack_formpost type usingwp_count_posts().totalItemsfrom an entity records query.Other information:
New store pieces:
SET_FORM_STATUS_COUNTS,INVALIDATE_FORM_STATUS_COUNTSsetFormStatusCounts(),invalidateFormStatusCounts()formStatusCountsgetFormStatusCounts()/wp/v2/jetpack-forms/status-counts, withshouldInvalidatesupportuseFormStatusCounts()— thin wrapper around the selector with defaultsDoes this pull request change what data or activity we track or use?
No.
Testing instructions:
jetpack_forms_alphaflag and thecentral-form-managementeditor feature flag.status-countsendpoint should be served from preloaded data (no extra request).Changelog