Skip to content

Forms: add standalone single response page#49827

Open
enejb wants to merge 3 commits into
trunkfrom
ship/forms-single-response
Open

Forms: add standalone single response page#49827
enejb wants to merge 3 commits into
trunkfrom
ship/forms-single-response

Conversation

@enejb

@enejb enejb commented Jun 22, 2026

Copy link
Copy Markdown
Member

Proposed changes

Adds a standalone, full-page view for a single Jetpack Forms response in the modern wp-build responses dashboard (routes/responses app, page jetpack-forms-responses-wp-admin).

  • New route routes/response/ → path /response/$responseId (reached at admin.php?page=jetpack-forms-responses-wp-admin&p=/response/<id>).
  • Not linked from anywhere yet — this is the screen only; wiring it into the inbox/row navigation will come in a follow-up.
  • Renders the response card only: person/metadata header and the question/answer fields (incl. file uploads).
  • Top bar: breadcrumb (Forms / <form> / #<id>) where the form crumb links to that form's filtered responses (shown only when the response is tied to a jetpack_form post); prev/next navigation (arrows + arrow-key shortcuts, hidden on mobile); and a single three-dot menu holding all actions (Mark as read/unread, Spam/Trash — or Restore/Delete by status — and Edit form). Status-changing actions navigate back to the relevant responses list.
  • Responsive: on mobile the card goes edge-to-edge (no outer frame), the background is uniform, and the prev/next arrows are hidden (keyboard nav still works).
  • Handles loading, not found, and loaded states.

Reuse: the response body reuses the existing inspector components (ResponseMeta, ResponseFieldsIterator, PreviewFile, ResponseNavigation), the page chrome (FormsPage), the inbox data loader (useInboxData), and the shared action factory (getActions). Only the route, the full-page layout, the breadcrumb/action bar, and the navigation hook are net-new.

Backend: one additive, read-only REST field — form_id on the feedback endpoint (the jetpack_form post ID, or 0 for classic forms) — used to decide whether to render and link the form breadcrumb.

Out of scope (intentionally deferred): the Activity card (private notes + system-event timeline) and linking the page from the inbox.

Known seams for a follow-up (left as-is to keep this diff isolated): the "mark as read on view" effect now exists in three spots and could be extracted into a shared hook; getActions lives under routes/responses/ but is consumed as a package-level utility; and the status→view map is duplicated. None are blockers for a not-yet-linked screen.

Related product discussion/links

Implements the provided single-response design (Activity card excluded for this first pass).

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

No new tracking or storage. The only backend change is one additive, read-only REST field (form_id) on the existing feedback endpoint, derived from the post's existing parent — no new data is collected. Uses existing feedback records, the existing /wp/v2/feedback/{id}/read endpoint, and the existing action callbacks.

Testing instructions

  1. On a test site with Jetpack Forms, submit a form a few times so you have some responses (or use existing ones).
  2. Find a response ID (e.g. from the responses list or wp post list --post_type=feedback).
  3. Visit …/wp-admin/admin.php?page=jetpack-forms-responses-wp-admin&p=/response/<id> (the modern dashboard is on by default).
  4. Confirm the page renders the breadcrumb, the response metadata + fields, and the action bar.
  5. Open the three-dot menu and exercise the actions: Mark as read/unread (stays on the page), Spam / Trash (navigates back to the list), Edit form.
  6. Use the prev/next arrows (or ArrowUp/ArrowDown) to move between responses. For a response tied to a managed form, confirm the breadcrumb shows the form name and links to that form's filtered responses.
  7. Narrow the window below 782px: the card should go edge-to-edge with a uniform background and the arrows should hide.
  8. Visit a non-existent ID (e.g. /response/99999999) and confirm the graceful "Response not found" state.

Verified on a Jurassic Ninja site with a form containing all field types and several varied submissions. This is a net-new screen, so there is no "before".

Screenshots

Captured on a live Jurassic Ninja site (desktop at 1440×900, mobile at 390×844).

Desktop — response tied to a form Desktop — classic (embedded) form
desktop, form response desktop, classic response
Mobile Not found
mobile not found

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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

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
Copy Markdown
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!

@jp-launch-control

jp-launch-control Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/packages/forms/src/contact-form/class-contact-form-endpoint.php 934/1107 (84.37%) 0.11% 0 💚

Full summary · PHP report · JS report

- Breadcrumb form crumb links to the form's filtered responses; expose a
  read-only `form_id` REST field (0 for classic/embedded forms) to drive it
- Add prev/next navigation (arrows + arrow-key shortcuts), hidden on mobile
- Consolidate all actions into the three-dot dropdown; drop Print and the
  Response ID block
- Consistent column width, uniform background on mobile, and fix the doubled
  bottom border on the last field
- Guard arrow-key navigation: only preventDefault when navigating, and ignore
  it while typing or while the file-preview modal is open
@enejb enejb requested review from a team, Copilot and dhasilva June 26, 2026 02:38
@enejb enejb marked this pull request as ready for review June 26, 2026 02:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new wp-build route under the Jetpack Forms responses dashboard to render a standalone, full-page view for an individual feedback response (including breadcrumbs, actions, and response content), and extends the feedback REST payload/type to expose the associated reusable form ID.

Changes:

  • Added a new /response/$responseId wp-build route bundle (stage, loader, breadcrumbs, actions, navigation hook, and styles).
  • Extended the feedback REST schema/response and TS types to include form_id for tying responses back to a jetpack_form post.
  • Added a Forms package changelog entry for the new screen.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
projects/packages/forms/src/types/index.ts Adds form_id to the FormResponse TS interface.
projects/packages/forms/src/contact-form/class-contact-form-endpoint.php Exposes form_id in the feedback REST schema and response payload.
projects/packages/forms/routes/response/use-navigation.ts Adds prev/next navigation logic based on the inbox data loader ordering.
projects/packages/forms/routes/response/style.scss Introduces standalone single-response page styling (layout, breadcrumbs, header nav).
projects/packages/forms/routes/response/stage.tsx Implements the standalone single-response page UI (loading/not-found/content + file preview modal).
projects/packages/forms/routes/response/route.tsx Adds a route loader to preload the response record (collection fields format).
projects/packages/forms/routes/response/page-actions.tsx Adds a top-bar actions dropdown for single-response operations.
projects/packages/forms/routes/response/package.json Registers the new route path and declares its route bundle dependencies.
projects/packages/forms/routes/response/breadcrumbs.tsx Implements custom breadcrumbs (including optional form-scoped crumb via form_id).
projects/packages/forms/changelog/add-forms-single-response-page Declares the user-facing changelog entry for the new standalone screen.

Comment on lines +53 to +64
const actions = useMemo( () => getActions( { navigate, searchParams: {} } ), [ navigate ] );
const currentView = VIEW_BY_STATUS[ response.status ] || 'inbox';

const runAction = useCallback(
async ( action: Action, navigateAway: boolean ) => {
await action.callback?.( [ response ], { registry } );
if ( navigateAway ) {
navigate( { to: `/responses/${ currentView }` } );
}
},
[ response, registry, navigate, currentView ]
);
Comment on lines +119 to +120
const groups: Control[][] = [ [ toggleRead ], statusControls ];

Comment on lines +21 to +28
try {
await resolveSelect( 'core' ).getEntityRecord( 'postType', 'feedback', id, {
fields_format: 'collection',
} );
} catch {
// Swallow fetch errors (e.g. 404 for a missing response) so the stage
// can render its own "not found" state instead of the router error boundary.
}
Comment on lines +220 to +223
<ResponseMeta response={ response } />

<ResponseFieldsIterator fields={ response.fields } onFilePreview={ handleFilePreview } />
</div>
Significance: minor
Type: added

Forms: add a standalone full-page view for a single form response.
search={ { sourceId: String( response.form_id ) } as unknown as never }
className="jp-forms__single-response-breadcrumbs__link"
>
{ formTitle }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it worth to add a getter for the actual form name when it's a managed form? Otherwise, this title usually fallbacks to the post/page title.

CGastrell
CGastrell previously approved these changes Jun 26, 2026

@CGastrell CGastrell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve

Verified locally: tsgo, eslint, wp-build (route registers correctly), php -l — all clean. Backend is additive/safe: get_form_id() already exists and is used in the same method, (int) casts the classic-form null to 0, and the schema test (assertArrayHasKey) isn't affected. Nice isolated diff.

One non-blocking suggestion — the breadcrumb uses response.entry_title, which is the embedding page's title, not the form's name. The crumb links to a list whose header shows the actual jetpack_form post title, so the two can disagree. Align it the same way the header does:

const formName = useSelect( select => {
    if ( ! response?.form_id ) return '';
    const rec = select( coreStore ).getEntityRecord( 'postType', 'jetpack_form', response.form_id );
    return rec ? decodeEntities( rec.title?.rendered || '' ) : '';
}, [ response?.form_id ] );
// pass `formName || formTitle` to <SingleResponseBreadcrumbs>

Only affects managed forms — classic responses have form_id = 0, so the crumb stays hidden and nothing changes. Fine to defer to the inbox-wiring follow-up.

- Breadcrumb shows the managed form's actual title (jetpack_form post name)
  instead of the embedding page title, matching the linked list's header
- Changelog: use a "Responses:" prefix instead of the package name
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