Skip to content

Conversation

@dhasilva
Copy link
Contributor

Fixes FORMS-386

Proposed changes:

  • Updates the inbox data to use local data if an item is moved to another tab
  • Fixes an issue where clicking "Undo" after trashing an item in spam would send it to the inbox instead
  • Optimistically sets the item status when clicking on an action to trash/restore/mark as spam/unmark as spam, making the UI update immediately

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:

  • Go to the dashboard
  • Use actions to send items to other tabs
  • Check that the action happens immediately
  • Click on "undo"
  • Check that it works as before (not immediately, but is undone)
  • Disable network and try an action
  • Check that the item is moved back after failure

@dhasilva dhasilva requested a review from a team November 13, 2025 00:02
@dhasilva dhasilva self-assigned this Nov 13, 2025
@dhasilva dhasilva added [Status] Needs Review This PR is ready for review. [Type] Task labels Nov 13, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 13, 2025

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-optimistic-move-entities branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/forms-optimistic-move-entities

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 a "[Type]" label (Bug, Enhancement, Janitorial, Task).
  • ✅ 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!

@jp-launch-control
Copy link

Code Coverage Summary

Coverage changed in 2 files.

File Coverage Δ% Δ Uncovered
projects/packages/forms/src/dashboard/inbox/dataviews/actions.tsx 0/252 (0.00%) 0.00% 12 💔
projects/packages/forms/src/dashboard/hooks/use-inbox-data.ts 40/76 (52.63%) -2.59% 6 💔

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

@vianasw vianasw requested a review from Copilot November 13, 2025 16:00
Copilot finished reviewing on behalf of vianasw November 13, 2025 16: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 implements optimistic UI updates for form response management in the Jetpack Forms dashboard. When users move items between inbox, spam, and trash, the UI updates immediately without waiting for the server response, providing a more responsive user experience.

Key Changes

  • Introduces optimistic status updates using editEntityRecord before API calls complete
  • Implements automatic rollback of optimistic updates when API calls fail
  • Adds filtering logic to show/hide items based on their effective (optimistically updated) status
  • Fixes the undo behavior to correctly restore items to their original status

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
projects/packages/forms/src/dashboard/inbox/dataviews/types.tsx Refactored type definitions to extract NoticeOptions, DispatchActions, and SelectActions types; added isUndo and targetStatus options to action callbacks
projects/packages/forms/src/dashboard/inbox/dataviews/actions.tsx Added withTimeout and processStatusChange helper functions; updated all action callbacks to support optimistic updates with rollback on failure and proper undo handling
projects/packages/forms/src/dashboard/hooks/use-inbox-data.ts Added filtering logic to exclude items that have been optimistically moved to different status tabs
projects/packages/forms/changelog/update-forms-optimistic-move-entities Added changelog entry documenting the feature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +336 to +341
itemsUpdated
.filter(
( promise ): promise is PromiseFulfilledResult< { id: number } > =>
promise.status === 'fulfilled' && !! promise.value
)
.map( ( { value } ) => value.id.toString() ),
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The itemsUpdated array is being filtered and mapped again here, but it's already been filtered in the processStatusChange function (lines 187-189). This creates inconsistency because processStatusChange returns items where promise.value is truthy, but here you filter again with different criteria.

More importantly, itemsUpdated returned from processStatusChange is typed as PromiseSettledResult<unknown>[], not specifically as fulfilled results with { id: number }. The type assertion at line 338 assumes the value has an id property, but the actual API response structure should be validated. Consider either:

  1. Making processStatusChange return properly typed results, or
  2. Adding runtime validation that the value contains the expected id property

Copilot uses AI. Check for mistakes.
Comment on lines +422 to +427
itemsUpdated
.filter(
( promise ): promise is PromiseFulfilledResult< { id: number } > =>
promise.status === 'fulfilled' && !! promise.value
)
.map( ( { value } ) => value.id.toString() ),
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Same issue as with markAsSpamAction: itemsUpdated returned from processStatusChange is typed as PromiseSettledResult<unknown>[], but here it's being treated as if it contains { id: number } values. The type assertion doesn't guarantee the runtime value structure matches. Consider adding runtime validation or properly typing the return value of processStatusChange.

Copilot uses AI. Check for mistakes.
items,
{ registry },
// We can trash a spam or inbox item, so we need to restore to the original status
{ isUndo: true, targetStatus: items[ 0 ]?.status }
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Using items[0]?.status as the targetStatus will not work correctly because at this point the items have already been optimistically updated to 'trash' status. The original status information is lost.

To fix this, you should capture the original status before the optimistic update (e.g., store it in a variable before calling processStatusChange) and use that stored value here instead of items[0]?.status.

Copilot uses AI. Check for mistakes.
restoreAction.callback( items, { registry } );
if ( ! isUndo ) {
// Reload the items to the store, as they were removed from the store when moved to trash
receiveEntityRecords( 'postType', 'feedback', items, queryParams, true );
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The receiveEntityRecords call should be done before the success notice to ensure the items are available in the store when the undo action is triggered. However, there's a more critical issue: this reloads the items with their current status after they've been optimistically updated to 'trash', which means the undo action will try to restore items from 'trash' to 'trash' (since items[0]?.status will be 'trash' at line 579).

The correct approach would be to capture the original statuses before the optimistic update and pass that information to the undo callback.

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +160
/**
* Helper function to determine the effective status of a record,
* considering optimistic edits (status field in edits).
*
* @param {FormResponse} record - The record to check.
* @return {string} The effective status of the record.
*/
const getEffectiveStatus = ( record: FormResponse ): string => {
// getEditedEntityRecord merges edits with the original record,
// so if status was edited, it will be in the edited record
return record.status || 'publish';
};
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The getEffectiveStatus function appears to be redundant. According to the comment, it claims to use getEditedEntityRecord to merge edits with the original record, but the function implementation just returns record.status directly.

Since editedRecords is already the result of calling getEditedEntityRecord on each record (as seen in the earlier code), the status should already include any optimistic edits. This helper function adds no value and the filtering could directly use record.status instead of calling getEffectiveStatus(record).

Copilot uses AI. Check for mistakes.
records: FormResponse[],
query?: QueryParams,
invalidateCache?: boolean
) => Promise< void >;
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The return type annotation for receiveEntityRecords specifies Promise<void>, but based on the WordPress core data APIs, receiveEntityRecords is typically a synchronous action creator that returns an action object, not a Promise. This type definition may cause type mismatches when the function is called.

Suggested change
) => Promise< void >;
) => void;

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +194
// Failed if rejected OR if fulfilled but result is invalid
if ( promise.status === 'rejected' || promise.value == null ) {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The condition promise.value == null uses loose equality which will match both null and undefined. This is intentional for checking invalid results, but the comment on line 194 says "if fulfilled but result is invalid" - it would be clearer to explicitly check promise.value === null || promise.value === undefined to make the intent more obvious, or add a comment explaining why loose equality is used here.

Suggested change
// Failed if rejected OR if fulfilled but result is invalid
if ( promise.status === 'rejected' || promise.value == null ) {
// Failed if rejected OR if fulfilled but result is null or undefined
if ( promise.status === 'rejected' || promise.value === null || promise.value === undefined ) {

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@vianasw vianasw left a comment

Choose a reason for hiding this comment

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

Very neat implementation. I tried very hard to break and force race conditions, but I couldn't. I tried with network throttling, breaking the connection, and forcing edge cases. So, despite Copilot's multiple comments that status is not properly handled on undo, it was working fine for me. I left a comment about documenting the optimistic update strategy, but feel free to merge without it.


/**
* Helper function to process status changes with optimistic updates and error handling.
*
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe would be good to explain here the optimistic update strategy. Something like:

/**
   * Optimistic Update Strategy:
   * 1. Immediately update local state and counts
   * 2. Make API call
   * 3. On success: invalidate cache to sync with server
   * 4. On failure: rollback local changes
   * 5. Undo actions must preserve original status for proper restoration
   */

@dhasilva
Copy link
Contributor Author

Will leave the comments for a follow-up, some things can be simplified or types can be enhanced.

@dhasilva dhasilva merged commit 32d6cbb into trunk Nov 13, 2025
78 of 80 checks passed
@dhasilva dhasilva deleted the update/forms-optimistic-move-entities branch November 13, 2025 20:09
@github-actions github-actions bot removed the [Status] Needs Review This PR is ready for review. label Nov 13, 2025
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