feat: save form submissions before delivery#2856
Merged
Merged
Conversation
Persist every valid submission as an otter_form_record before any delivery action runs, so a failed email/integration can no longer lose the data.
- move core storage (CPT, save hook, submissions dashboard, /form/confirm)
from Otter Pro to lite; Pro keeps filter, export and Stripe steps
- replace the Save Location selector with an Email Notification toggle; legacy submissionsSaveLocation values are migrated at read time and rewritten on the next editor save
- record delivery_status/delivery_errors meta on each Record
and surface them in the dashboard list and detail views
- treat captcha provider outages as infrastructure failures: save the Record, skip delivery, alert the admin (also fixes a latent fatal on WP_Error responses in check_form_captcha)
- throttle admin alerts to one per form per hour per failure type
- add unit suites (form server, submissions storage, Pro delivery) and a form-retention e2e spec with scenario tooling in the e2e mu-plugin
Contributor
Bundle Size Diff
|
Contributor
|
Plugin build for 3152c9f is ready 🛎️!
|
Contributor
E2E TestsPlaywright Test Status: See serial and parallel matrix jobs Performance ResultsserverResponse: {"q25":462.3,"q50":464.4,"q75":494.5,"cnt":10}, firstPaint: {"q25":571.4,"q50":650.25,"q75":730,"cnt":10}, domContentLoaded: {"q25":3469,"q50":3513.15,"q75":3529.3,"cnt":10}, loaded: {"q25":3471.1,"q50":3515.05,"q75":3531.1,"cnt":10}, firstContentfulPaint: {"q25":4010.2,"q50":4052.35,"q75":4089.5,"cnt":10}, firstBlock: {"q25":13691.1,"q50":13763.25,"q75":13860.9,"cnt":10}, type: {"q25":22.97,"q50":23.58,"q75":25.64,"cnt":10}, typeWithoutInspector: {"q25":18.88,"q50":20.34,"q75":25.17,"cnt":10}, typeWithTopToolbar: {"q25":27.71,"q50":30.22,"q75":33.3,"cnt":10}, typeContainer: {"q25":13.09,"q50":13.8,"q75":14.85,"cnt":10}, focus: {"q25":105.63,"q50":107.12,"q75":112.71,"cnt":10}, inserterOpen: {"q25":36.07,"q50":36.93,"q75":38.89,"cnt":10}, inserterSearch: {"q25":12.42,"q50":12.64,"q75":12.89,"cnt":10}, inserterHover: {"q25":5,"q50":5.2,"q75":5.6,"cnt":20}, loadPatterns: {"q25":1510.91,"q50":1560.18,"q75":1660.56,"cnt":10}, listViewOpen: {"q25":215.59,"q50":216.65,"q75":223.96,"cnt":10} |
- fix JSDoc errors in e2e helpers (param names, alignment) - drop stale phpstan baseline entries for removed transition_draft_to_read() - ignore deliberate method_exists() guard for older Otter Pro versions - add WP_REST_Request generic to replace_corrupted_form_data() docblock - ignore intentional meta_query usage in bounded record lookups - e2e: close the font family popover while fonts load; rendering the full font list starves actionability checks on slow CI runners Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_the_title() returns an empty string for untitled pages, leaving the Post column link with no visible text. Mirror the core list-table convention instead, and fall back to the stored post_url when the resolved permalink is empty (deleted source post). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abaicus
approved these changes
Jun 16, 2026
abaicus
left a comment
Contributor
There was a problem hiding this comment.
Things look good, @Soare-Robert-Daniel , I just have a suggestion to maybe split the form submissions class if possible and it makes sense.
Contributor
There was a problem hiding this comment.
Is there a chance we could maybe split this file a bit by concerns? If not, it's ok.
Move uploaded-file cleanup (delete on record delete + path helpers) into a dedicated Form_Records_Files class that registers its own before_delete_post hook. Behaviour unchanged; covered by the existing permanent-delete tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the Pro list-table filter UI (form/post dropdowns, parse_query handler, locked upsell) into a dedicated Form_Records_Filters class registering its own restrict_manage_posts/parse_query hooks. Add a characterization test for the Pro filter query via the parse_query hook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the single-record edit screen (submission-data meta box, Update box with delivery status, admin-menu placement, field save handler) into a dedicated Form_Records_Meta_Box class registering its own add_meta_boxes/admin_menu/ save_post hooks. Add a characterization test for saving edited field values via the save_post hook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the Submissions list-table presentation (columns, sortable columns, bulk/row-action definitions, per-column values, delivery badge, unread-row styling) into a dedicated Form_Records_List_Table class. Status mutations (bulk handler, untrash restore) stay in Form_Submissions. Add a characterization test for the delivery column via the custom-column hook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the otter_form_record post type, its read/unread statuses and the admin capability grants into a dedicated Form_Records_Post_Type class registering its own init/admin_init hooks. Update the test setup to grant caps via the new class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the admin-ajax bulk export handler into a dedicated Form_Records_Export class registering its own wp_ajax hook, and drop the now-unused Pro import from Form_Submissions. Update the export test to call the new class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rm-retention # Conflicts: # src/blocks/test/e2e/blocks/ai-block.spec.js
…rm-retention # Conflicts: # phpstan-baseline.neon # src/blocks/test/e2e/blocks/ai-block.spec.js
Integrate the Cloudflare Turnstile captcha feature (#2832) with the form submission retention work. Conflict resolutions: - class-form-server.php: union the captcha-verification guard (form_has_captcha() + token present + no prior error) and keep the infrastructure-failure handling (mark_infrastructure_failure on is_wp_error/non-200), which subsumes development's simpler is_wp_error check. - otter-e2e-bootstrap.php: keep both captcha mocks — scenario-mode mock_captcha_provider (recaptcha) and stub_captcha_http_verification_for_e2e (recaptcha + turnstile). The /captcha endpoint now also sets a dummy recaptcha secret for active scenario modes so the merged empty($secret) gate doesn't short-circuit. - playwright.config.js: keep both serial specs (form-retention, form-turnstile). Also fix a pre-existing race in the form block surfaced by the merged behavior: a late/duplicate server load could migrate the legacy submissionsSaveLocation value back over an Email Notification toggle the user had just changed. A notificationDirty ref now preserves the edited value once dirty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2775
Summary
Persist every valid submission as an otter_form_record before any delivery action runs, so a failed email/integration can no longer lose the data.
Screenshots
Test instructions
Checklist before the final review