Skip to content

Conversation

@simison
Copy link
Member

@simison simison commented Jan 16, 2026

Follow up to FORMS-442

Proposed changes:

  • Move actions to their own file, and bring over previous functionality from older dashboard.

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

Does this pull request change what data or activity we track or use?

Testing instructions:

  • Use all actions from the rows (not the sidebar)
  • Pay attention to having filters set
  • Actions on 2nd page when the list "empties" due to action don't yet update as they should; we should adapt to use new navigate() from router

page: targetPage,
};

setCurrentQuery( updatedQuery );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: use navigate() from new router

@github-actions
Copy link
Contributor

github-actions bot commented Jan 16, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the update/forms-wp-build-dashboard-actions branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/forms-wp-build-dashboard-actions

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

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:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

const markAsSpamAction: Action< FormResponse > = {
id: 'mark-as-spam',
isPrimary: true,
icon: <Icon icon={ spam } />,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we need icons anymore with latest DataViews? TODO: check if bulk actions need them

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The footer bulk actions used to need them, but not anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still used by multi actions on mobile

image

@jp-launch-control
Copy link

jp-launch-control bot commented Jan 16, 2026

Code Coverage Summary

This PR did not change code coverage!

That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷

Full summary · PHP report · JS report

@simison
Copy link
Member Author

simison commented Jan 19, 2026

TODO: bring over #46663 (done in 4a40787)

@simison simison force-pushed the update/forms-wp-build-dashboard-actions branch from 648f1ce to 9ee44c0 Compare January 20, 2026 10:29
---------

Co-authored-by: Christian Gastrell <cgastrell@gmail.com>
@simison simison force-pushed the update/forms-wp-build-dashboard-actions branch from 9ee44c0 to 4a40787 Compare January 20, 2026 10:34
@simison simison marked this pull request as ready for review January 20, 2026 11:03
Copilot AI review requested due to automatic review settings January 20, 2026 11:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors form response actions from inline implementations in stage.tsx to a separate actions.tsx file to improve code organization for the WordPress build dashboard.

Changes:

  • Extracted action handlers from stage.tsx into a new actions.tsx file
  • Created a getActions() function that returns actions based on the current view
  • Removed unused imports and type dependencies from stage.tsx

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.

File Description
projects/packages/forms/routes/responses/stage.tsx Removed inline action implementations and replaced with call to getActions() function; cleaned up unused imports
projects/packages/forms/routes/responses/actions.tsx New file containing all action implementations with optimistic updates, error handling, and undo functionality
projects/packages/forms/changelog/update-forms-wp-build-dashboard-actions Added changelog entry for the refactoring

navigate,
searchParams,
view: params.view,
getItemId,
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter getItemId is passed to the getActions function but is not used in the implementation. The function parameter should be removed from both the call site and the function signature to avoid confusion.

Suggested change
getItemId,

Copilot uses AI. Check for mistakes.
);

// Check for both rejected promises and fulfilled promises with undefined/invalid results
// const itemsUpdated: PromiseFulfilledResult< { id: number } >[] = [];
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented-out code should be removed. The active implementation on line 250 is the correct one.

Suggested change
// const itemsUpdated: PromiseFulfilledResult< { id: number } >[] = [];

Copilot uses AI. Check for mistakes.
Comment on lines +419 to +422
let status = 'inbox';
if ( items[ 0 ]?.status === 'trash' ) {
status = 'trash';
}
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status detection logic assumes all items have the same status by checking only items[0]?.status. In bulk operations, items could potentially have different statuses. Consider checking if all items have the 'trash' status, or handling mixed statuses more robustly.

Suggested change
let status = 'inbox';
if ( items[ 0 ]?.status === 'trash' ) {
status = 'trash';
}
// Treat the view as "trash" only if all successfully updated items were in trash.
const allItemsFromTrash = itemsUpdated.every(
( item: FormResponse ) => item.status === 'trash'
);
const status = allItemsFromTrash ? 'trash' : 'inbox';

Copilot uses AI. Check for mistakes.
Comment on lines +920 to +983
items.forEach( () => {
updateCountsOptimistically( 'trash', 'deleted', 1, queryParams );
} );

const promises = await Promise.allSettled(
items.map( ( { id } ) =>
deleteEntityRecord( 'postType', 'feedback', id, { force: true }, { throwOnError: true } )
)
);

const itemsUpdated = promises.filter( ( { status } ) => status === 'fulfilled' );

// If there is at least one successful update, invalidate the cache for filters.
if ( itemsUpdated.length ) {
invalidateFilters();
invalidateCacheAndNavigate( registry, getCurrentQuery(), queryParams, 'trash' );
}

if ( itemsUpdated.length === items.length ) {
// Every request was successful.
const successMessage =
items.length === 1
? __( 'Response deleted permanently.', 'jetpack-forms' )
: sprintf(
/* translators: %d: the number of responses. */
_n(
'%d response deleted permanently.',
'%d responses deleted permanently.',
items.length,
'jetpack-forms'
),
items.length
);

createSuccessNotice( successMessage, { type: 'snackbar', id: 'move-to-trash-action' } );

// Update the URL to remove references to deleted items.
// Parse the hash to extract just the query params (e.g., #/responses?r=1,2,3)
const hash = window.location.hash;
const hashQueryIndex = hash.indexOf( '?' );
const hashBase = hashQueryIndex > 0 ? hash.substring( 0, hashQueryIndex ) : hash;
const hashQuery = hashQueryIndex > 0 ? hash.substring( hashQueryIndex + 1 ) : '';

const hashParams = new URLSearchParams( hashQuery );
const currentSelection = hashParams.get( 'r' )?.split( ',' ) || [];
const deletedIds = items.map( ( { id } ) => id.toString() );
const newSelection = currentSelection.filter( id => ! deletedIds.includes( id ) );

if ( newSelection.length ) {
hashParams.set( 'r', newSelection.join( ',' ) );
} else {
hashParams.delete( 'r' );
}

const hashString = hashParams.toString();
window.location.hash = hashString ? `${ hashBase }?${ hashString }` : hashBase;

return;
}
// There is at least one failure.
const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length;
const errorMessage = getGenericErrorMessage( numberOfErrors );

createErrorNotice( errorMessage, { type: 'snackbar' } );
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleteAction performs optimistic count updates on line 920-922 for all items but does not revert these counts if some deletions fail. If some items fail to delete (checked on line 938), the counts will be incorrect. Consider reverting the optimistic count updates for failed items.

Copilot uses AI. Check for mistakes.
Comment on lines +1062 to +1089
if ( promises.every( ( { status } ) => status === 'fulfilled' ) ) {
// Every request was successful.
const successMessage =
items.length === 1
? __( 'Response marked as read.', 'jetpack-forms' )
: sprintf(
/* translators: %d: the number of responses. */
_n(
'%d response marked as read.',
'%d responses marked as read.',
items.length,
'jetpack-forms'
),
items.length
);

createSuccessNotice( successMessage, {
type: 'snackbar',
id: 'mark-as-read-action',
actions: [
{
label: __( 'Undo', 'jetpack-forms' ),
onClick: () => {
markAsUnreadAction.callback( items, { registry } );
},
},
],
} );
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success notice is shown when all promises are fulfilled (line 1062), but the undo action on line 1085 attempts to undo all items, including those that may have failed. If some items failed to be marked as read, undoing all items would incorrectly attempt to mark the failed items as unread. Consider only including successfully updated items in the undo callback.

Copilot uses AI. Check for mistakes.
Comment on lines +425 to +427
queryParams,
} ),
[ navigate, searchParams, params.view, queryParams ]
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter queryParams is passed to the getActions function but is never used in the implementation. Each action obtains queryParams dynamically by calling getCountQueryParams(getCurrentQuery()) inside the callback. This unused parameter should be removed from both the call site and the function signature.

Suggested change
queryParams,
} ),
[ navigate, searchParams, params.view, queryParams ]
} ),
[ navigate, searchParams, params.view ]

Copilot uses AI. Check for mistakes.
// If there is at least one successful update, invalidate the cache and navigate if needed
if ( itemsUpdated.length ) {
let status = 'inbox';
if ( items[ 0 ]?.status === 'trash' ) {
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status detection logic assumes all items have the same status by checking only items[0]?.status. In bulk operations, items could potentially have different statuses. Consider checking if all items have the 'trash' status, or handling mixed statuses more robustly.

Suggested change
if ( items[ 0 ]?.status === 'trash' ) {
const allItemsInTrash = itemsUpdated.every(
item => item.status === 'trash'
);
if ( allItemsInTrash ) {

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need, all items are guaranteed to have the same status, at least for now.

const { getCurrentQuery } = registry.select( dashboardStore );

const queryParams = getCountQueryParams( getCurrentQuery() );
const previousStatus = items[ 0 ]?.status; // All items have the same status
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "All items have the same status" assumes a constraint that may not be enforced. While the isEligible check on line 764 ensures individual items are not 'trash', in a bulk operation items could still have different statuses (e.g., 'publish', 'spam', 'draft'). This could lead to incorrect undo behavior on line 869 where previousStatus is used for all items.

Suggested change
const previousStatus = items[ 0 ]?.status; // All items have the same status
const firstItemStatus = items[ 0 ]?.status;
const hasMixedStatus = items.some( item => item.status !== firstItemStatus );
if ( hasMixedStatus ) {
createErrorNotice(
__(
'Unable to move responses with different statuses to trash at once.',
'jetpack-forms'
),
{ type: 'snackbar' }
);
removeNotice( 'move-to-trash-action' );
return;
}
const previousStatus = firstItemStatus;

Copilot uses AI. Check for mistakes.
items.length
);

createSuccessNotice( successMessage, { type: 'snackbar', id: 'move-to-trash-action' } );
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notice ID 'move-to-trash-action' is used for the deleteAction, but this ID is already used by the moveToTrashAction. This could cause notice conflicts. Consider using a unique ID like 'delete-action' or 'delete-permanently-action'.

Suggested change
createSuccessNotice( successMessage, { type: 'snackbar', id: 'move-to-trash-action' } );
createSuccessNotice( successMessage, {
type: 'snackbar',
id: 'delete-permanently-action',
} );

Copilot uses AI. Check for mistakes.
Comment on lines +1165 to +1169
if ( promises.every( ( { status } ) => status === 'fulfilled' ) ) {
// Invalidate counts cache to ensure counts are refetched and stay accurate
invalidateCounts();
// Mark successfully updated records as invalid instead of removing from view
const updatedIds = items.map( item => item.id );
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency in when counts are invalidated. The markAsReadAction invalidates counts if any items succeed (line 1051), but markAsUnreadAction only invalidates counts if all items succeed (line 1165-1167). For consistency and correctness, both should invalidate counts whenever at least one item succeeds.

Suggested change
if ( promises.every( ( { status } ) => status === 'fulfilled' ) ) {
// Invalidate counts cache to ensure counts are refetched and stay accurate
invalidateCounts();
// Mark successfully updated records as invalid instead of removing from view
const updatedIds = items.map( item => item.id );
const updatedIds = promises.reduce( ( acc, { status }, index ) => {
if ( status === 'fulfilled' ) {
acc.push( items[ index ].id );
}
return acc;
}, [] );
if ( updatedIds.length > 0 ) {
// Invalidate counts cache to ensure counts are refetched and stay accurate
invalidateCounts();
// Mark successfully updated records as invalid instead of removing from view

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants