== Changelog == = 2.3.6 = * Fix: the color pickers of the plugin settings, Layout & Styling tab, kept the first color chosen and silently discarded every later one, and the swatch beside the button stayed blank instead of showing the current color. They were initialized with a change callback that fired a jQuery `change` on their own input. Iris binds a listener to `change` on that same input, and the callback runs before Iris has written the new color back into the field, so Iris re-read the value still sitting there, found the previous color and set that instead. From an empty field there was nothing to revert to, but the same event reached wpColorPicker's own change handler, which clears the swatch background when the value reads empty. Nothing on that screen listens for `change` on those inputs, so the callback was doing no work of its own and is removed; the widget now behaves as WordPress ships it. JS version 138. * Fix: in the form editor, Appearance and General tab, clicking the color swatch often left the panel closed and the following click opened it, which read as a missed first click. The screen carried its own "only one picker open at a time" handling, which hid the Iris panel and removed the widget's active class directly. `open()` and `close()` both call `iris('toggle')`, which flips on the element's current visibility rather than on the widget's own state, so a panel hidden without the widget's knowledge inverted the next click and left one picker marked open with its panel hidden while another was marked closed with its panel showing. That handling is removed rather than repaired, because `open()` already closes every other picker before it opens its own. Iris's one-shot "show the panel on first focus" handler is dropped as well: it shows the panel without the widget marking it open, and it is only reachable on this screen because the color code box is kept visible at all times. The swatch button opens the panel, and typing a color code still works through Iris's own change and keyup listeners. * Verification: new `color-pickers.spec.ts`, eight tests across both screens (a second pick replaces the first; the swatch follows the picked color; the picked colors survive a save and a reload; Clear empties the field; the swatch alternates open and closed over repeated clicks; clicking another picker's color code box leaves the open one consistent; opening one picker closes the other; a second pick replaces the first and saves). Three fail against the unchanged code, one for each symptom; the other five cover behaviour that was already correct and pass either way by design. Full admin suite green at 137 passed, 4 skipped. * Verification: `restoreOption()` in the test helpers restored nothing at all for an option over roughly 4 KB, so a spec that used it left its own values in the database and still reported green. It passed the JSON inline to `wp eval-file`, which silently evaluates nothing when a file holds a single line that long. The payload now goes to a file of its own, is read with `file_get_contents()`, and the helper raises unless the restore reports success. * i18n: no new or changed strings, so both catalogs are unaffected. = 2.3.5 = * Feature: the two Excel exports of the submissions list are also bulk actions, applied to the submissions that are ticked rather than to the whole list. `get_bulk_actions()` offers `export_visible` and `export_all` in every view, under the labels the buttons above the table already carry, so the dropdown needed no new translation. The file is written by the same code and over the same view: the filters, the search and the sort order on screen all still apply, and only the set of rows is narrowed. The column set is resolved on the server, since a bulk action arrives as a plain form submit rather than through the browser, and it matches what Screen Options shows. The export runs on the page's load hook rather than alongside the other bulk actions, which run once the response has begun and so cannot send a file. An empty selection is refused, by WordPress in the browser and by the plugin with the usual "No submission selected." notice. The two buttons above the table are untouched and still export the whole filtered view. No JavaScript was added, so the feature also works with scripting off, and no asset versions changed. * Verification: `submissions-list.spec.ts` gained five tests (the dropdown offers both exports and one of them writes the ticked rows and no others; "all columns" carries a column that "visible columns" leaves out; an empty selection is stopped in the browser; the same request made without that guard is answered with the notice rather than a file; and the list still shows every row when a previous row action has left its parameters in the address bar). Four of them fail against the unchanged code. The fifth guards against a regression rather than describing the feature, so it passes either way by design. Full suite green at 233 passed, 4 skipped. * i18n: no new strings, so the Italian and Spanish catalogs were already complete. Their source references were re-synced against a regenerated POT, which stayed at 703 entries. = 2.3.4 = * Feature: a search box on the Fields page, built on `WP_List_Table::search_box()` so it is the same control the Categories and Tags screens use. It sits in its own GET form, the way `edit-tags.php` does it: sharing the table's form would serialize the bulk-action selects, their nonce and the referer into the URL on every search, and the URL stays `?page=accua_forms_fields&s=term`. Matching covers the label, the slug, the field type and the description, and the type is matched on its translated name as well as its key, so "Casella di controllo" finds a checkbox field on an Italian install. Case-insensitive through `mb_stripos()` where mbstring is available, `stripos()` otherwise. Searching and sorting combine: `search_box()` carries the current orderby and order, and the sort links carry the search. The table now declares its pagination arguments, which is what prints the "N items" count above it, the feedback a search needs; page links only appear past 50 fields, and that number goes through the standard `accua_forms_fields_per_page` filter and screen option. An empty result says so rather than showing a bare table. * Verification: `fields-page.spec.ts` gained five tests for the search (the box narrows the list by label; the slug and the description are searched too; the count reflects the matches rather than the whole list; sorting keeps the search; an unmatched term says so and leaves the box usable). All five were checked against the unfiltered code first and all five fail there, so they guard the behaviour rather than merely describing it. * i18n: POT regenerated at 703 entries, and the Italian and Spanish catalogs are complete against it, with the four new strings the search adds. Those are the only two translations the plugin ships. Their headers were repaired at the same time: `Language` had been empty and `Plural-Forms` missing since 2013, so plurals were resolving on gettext's default rule rather than a declared one (correct for both languages, but by luck), and one leftover Poedit path line was malformed enough to parse as a header field called `D`. * Fix: both live previews, the form editor's and the Fields page's, logged a PHP deprecation notice on every refresh from WordPress 6.4 on. They build a standalone document and call `wp_print_styles()` directly, which fires the `wp_print_styles` action, and core still keeps the deprecated `print_emoji_styles()` on that action for backward compatibility. Core retires it inside `wp_enqueue_emoji_styles()`, which runs on `wp_enqueue_scripts` or `admin_enqueue_scripts`, and neither of those runs in an AJAX request, so the deprecated printer was still attached. Both previews now do what a normal page load would have done, through the shared `accua_forms_prepare_emoji_styles_for_preview()`: drop the deprecated hook and enqueue the emoji styles the current way, so the preview keeps exactly the CSS it had. A no-op on WordPress 5.9 to 6.3, where `print_emoji_styles()` is still the current API and there is nothing to replace. Measured on the dev stack: a full end to end run wrote about 150 of these notices before and writes none after, with the whole log down to 2 unrelated lines. = 2.3.3 = * Tested up to WordPress 7.1. Two of its changes reach this plugin, and neither needed a fix. `WP_List_Table::single_row_columns()` now renders the checkbox cell as a `td` where it was a `th`, and the primary column as a `th scope="row"`, which applies to the Forms, Fields and Submissions tables through the base class; core's own admin CSS normalizes the new `th` back to the weight, size and alignment the `td` had (`.widefat th:not(tbody th)` for the font size, `.widefat tbody th` for vertical alignment), so the three tables are unchanged to look at and their rows gain the row header they were missing. The other change is jQuery UI 1.13.3 to 1.14.2, which the form editor's drag and drop is built on: dragging a field in from the palette, reordering fields inside the form, and the resizable preview were driven with real mouse events on 7.1 and on 7.0.4 and behaved identically, helper and placeholder included. What 1.14 removes (`$.ui.ie`, `$.ui.safeActiveElement`, `$.ui.safeBlur`, and droppable's `activeClass`/`hoverClass`, now behind `$.uiBackCompat === true`) is not used by this plugin or by any of the seventeen extensions. The three functions 7.1 deprecates (`delete_users_add_js()`, `_wp_has_noncharacters_fallback()`, and a WP_HTML_Open_Elements method) are not called either. WordPress 7.1 still requires PHP 7.4, so `Requires at least: 5.9` and `Requires PHP: 7.4` are unchanged. * Fix: the fade-out of a deleted row on the Forms list matched the checkbox cell as `th.check-column`, which is a `td` from WordPress 7.1 on. It matches by class now, so it is right on every version. No visible effect either way: the bulk action removes the form before the table is prepared, so the row it looks for is already gone. * Verification: the full E2E suite was run on 7.0.4 first, for a baseline to compare against, then on 7.1: 219 passed on 7.0.4 before the change, 223 passed on 7.1 after it, 4 skipped either way, and 219 again on 7.0.4 with the change in, so nothing regressed on the older version. The declared minimums were re-proved on WordPress 5.9 with PHP 7.4 (fresh install, submissions through the real handler, the five admin pages, and both uninstall paths). `debug.log` carries the same entries on both WordPress versions, so none of them are new. * Verification: new `form-editor-dnd.spec.ts` covers what the suite never did, the form editor's drag and drop, which is the part a jQuery UI upgrade in WordPress can break. Four tests: the jQuery UI widgets attach (asserting that a version is reported, not which one), a field can be dragged in from the palette, a field can be reordered without being lost or duplicated, and the preview resizable plus the static submit row staying last. Green on 7.0.4 and on 7.1. It drives real mouse events, since jQuery UI does not react to synthetic ones. = 2.3.2 = * Feature: a "Create new field" link at the end of the Available Fields column of the form editor, pointing at the Fields page. The column scrolls with its list, so the link sits inside it, after the last field, rather than pinned under the column. CSS version 207. * Fix: every click of Save in the form editor wrote a PHP warning and three deprecation notices to the error log. The static "Default Submit button" placeholder in the drop zone has no field settings of its own, so the editor was sending a save request for it with nothing in it for the handler to act on; nothing was saved either way, and `save()` and `saveSilent()` now skip that widget instead of sending the request. The outcome is identical and deletes are unaffected. The handlers involved no longer assume the request is complete. JS version 137. * Fix: a blocklist entry using several wildcards could silently stop blocking longer addresses, so a sender on the list could still get through. Matching is now a single left to right walk (`accua_forms_glob_matches()`) and gives the same answer whatever the length of the address or the number of wildcards in the entry. What an entry means is unchanged, and it stays byte-wise. Runs of consecutive `*` are also collapsed to one, on save and at match time, which is what they already meant. * Fix: `accua_forms_spam_settings()` raised "Array to string conversion" if `accua_forms_spam_data` held the blocklist as an array rather than as the text the settings page stores, which an option written by hand or by a migration can. An array is folded back into lines and any other non-string is cast, so the list is read instead of breaking its callers. * Verification: form-editor.spec.ts asserts that a full save sends no incomplete request (it fails against the previous JS); spam-blocklist.spec.ts covers the wildcard semantics, including the entries that used to stop matching, and the array-shaped option. Both files green, 28 tests. = 2.3.1 = * Feature: "Auto mark as spam", a site-wide list of email addresses that are always treated as spam, in a new "Other settings" tab of the plugin settings. A submission carrying one of the listed addresses in an email field is classified as spam whatever the captcha of that form decided - a captcha can tell how a form was filled in, not who filled it in, so a sender already known to be unwanted solves it and gets through. Entries can be an exact address (`spammer@example.com`), a whole domain (`*@spammydomain.net`, or just `spammydomain.net`), its subdomains (`*@*.spammydomain.net`) or any wildcard pattern (`*` for any run of characters, `?` for exactly one); lines starting with `#` are comments. Matching ignores upper and lower case and covers every Email and Autoreply Email field of the form. A match is accepted silently: the visitor sees the usual success message, the submission is stored with the Spam lead status and no notification or autoreply email is sent. It is also the simplest way to see the spam handling at work - put an address you can send from on the list and submit one of your own forms with it. Empty by default, so nothing changes until an address is added; the check short-circuits before it looks at a single submitted value while the list is empty. * Implementation: `includes/spam-blocklist.php`, hook-free plain functions like `includes/data-deletion.php`. New option `accua_forms_spam_data` (key `email_blocklist`), storing the text as typed rather than an array so the textarea round-trips it with its order and comments; added to the data-deletion option list. Filters `accua_forms_email_blocklist`, `accua_forms_email_is_blocklisted` and `accua_forms_spam_email_field_types`. Patterns are matched against the whole address, anchored at both ends. Entries that would silently classify every submission carrying an address (`*`, `*@*`, `?`) are refused rather than stored, and so is a bare word with no `@`, no dot and no wildcard, which would otherwise be read as a domain - a pasted "Name
" is unwrapped before the line is split, so the display name cannot become entries of its own. * Implementation: the blocklist only upgrades a submission the captcha let through; a captcha classification that is already stricter (Trash, delete) stands. The reason is recorded in the existing `afs_stats` JSON as `spam_reason` (the reason only, never the address - `afs_stats` is read on every view of the submission and is cleared on anonymization) and shown as a "Spam check" row on the submission page, driven by what that submission stored, so it keeps saying so after the blocklist entry that caught it is removed. * Implementation: `accua_forms_captcha_spam_submission` and its deprecated alias `accua_forms_recaptcha3_spam_submission` now fire only for captcha classifications, as their names and documentation say - code acting on them (feeding a captcha's own reporting API, tuning a score threshold) must not see a submission no captcha ever looked at. The new `accua_forms_spam_submission` ($submission_id, $spam_action, $fid, $spam_reason) fires for every silent classification, whichever check made it. Existing listeners are unaffected: every captcha path reaches them exactly as before. * Fix (user-reported, seen on a client site): with enough columns showing, the submissions list scrolled the whole admin page sideways instead of scrolling the table. The filters, the pagination and the heading slid out of view, the fixed admin bar stopped short of the content, and on sites whose admin menu is taller than the page the menu links slid away while their blue background stayed behind. The table is now wrapped in its own scroll box, and a scrollbar pinned to the bottom of the viewport scrolls it without having to reach the foot of a hundred rows. Column widths keep adapting to their content. * Fix: the reCAPTCHA v3 badge set to "Bottom left" rendered cut off against the left edge, showing the tail of "protected by reCAPTCHA" instead of the logo. The badge is a single 256px strip that Google collapses by pushing 186px of it off screen, so CSS that moves the whole strip to the left edge can only leave the wrong half of it visible - the position has to come from Google, which builds the mirrored strip for that side. `bottomleft` is now passed as the `badge` parameter of `grecaptcha.render()`, which needs api.js in explicit-render mode: the loader requests `render=explicit` for that setting, renders one invisible widget per site key (so several v3 fields on a page still share a single badge) as soon as the API is ready, and executes against the widget id. Rendering has to happen at that point, not when the token is fetched: in explicit-render mode api.js paints nothing by itself, so a widget created at submit time would put the badge on screen only after the form had been sent. `bottomright` and `hidden` keep the previous `render=` path untouched, and a refused `render()` falls back to executing against the site key, so a badge that cannot be placed never costs the token. The CSS the field used to print for `bottomleft` is gone; `hidden` still prints its `visibility:hidden` rule and the required notice. The badge choice now travels to the loader in the field's JS registration, which pages cached with the older markup simply omit. JS version 136. * Change: em dashes removed from the readme, the changelogs and the plugin, in code comments and in visible text alike, along with the en dashes used as separators in the lead-status legend and the client-side error summary. The translatable strings affected (the captcha spam-action help, the retention mode labels, the blocklist note, the block editor's form placeholder) kept their translations across the change; the block placeholder lost its decorative dashes and now reads "Select a form". * Italian and Spanish translations updated. = 2.3.0 = * Fix: "Delete all data" (the Danger Zone button and the deactivation modal) left several things behind - the routine had drifted away from the plugin it cleans. **Uploaded files were the worst of it**: the destination was read from `accua_forms_file_data`, a name the plugin stopped writing long ago, so it always resolved to the default directory. A site with a configured upload path therefore had that default directory wiped while every file its visitors had submitted stayed on disk. The path now comes from the option the uploads really use (`accua_forms_default_file_field_data`), and the default directory is cleaned as well since files predating a custom path still live there. Because that makes the deletion actually follow an administrator-set path, it is now refused for any directory that is not plugin-owned - the uploads root, wp-content, wp-admin, the WordPress root or anything outside the install - so a destination like "wp-content/uploads" can never take the media library with it. * Fix: four options survived "Delete all data": the captcha settings (which hold the reCAPTCHA and Cap **secret keys**), the analytics settings, the file-field settings and the recovery backup of the field list. The option list now covers everything the plugin writes, and keeps the names used by older versions so a long-lived install is cleaned out too. The cached dashboard statistics (a transient holding submission counts) and the per-user submissions-list preferences (per-page choice and hidden columns) are removed as well. Matching is deliberately narrow: an extension plugin's own options, tables and user meta are never touched. * Improved: the reCAPTCHA v3 badge position setting says when its effect is visible. The badge only appears once the visitor starts filling the form, because no request is made to Google before that, and the CSS that positions or hides it is printed with the form - so a page cache keeps serving the previous position until it is emptied. Both are now stated under the setting; without that, changing the position looks like it does nothing. * Fix: the values the plugin stores for its own use were offered as submission columns. The field columns of the submissions list come from `SELECT DISTINCT afsv_field_id`, which also returns internal rows - notably `_accua_download_token`, the token that authorizes a file download - so they appeared as "(removed)" columns in the list and in Screen Options, and their values went into the Excel export. Column keys with the reserved `__` prefix (which the Fields page refuses as a slug) or the internal `_accua_` prefix are now skipped, the same two prefixes the single submission page has always skipped. * The DB 18 collation conversion is applied on every install run - every plugin activation as well as every version change - rather than only when the stored DB version is older. `dbDelta()` rewrites `afsv_field_id` back to the table's default (case-insensitive) collation whenever it runs, so a version-gated conversion would be lost again at the next activation, taking the case-sensitive slugs with it. The conversion returns early when the collation is already binary, so re-checking it costs one information_schema lookup. * Fix (DB 19): forms carried over from a pre-2.0 installation saved submissions with **no field values at all**. The 1.x -> 2.0 conversion, unchanged since 2.0.0, rebuilt a form's comma-separated field list as instances that had no instance id and sat under numeric keys. Such a form renders perfectly (the render loop reads the field reference) and the submission is recorded, but every value is dropped, because the submission handler matches each posted field against the instance keys and finds nothing. The conversion now produces instances keyed by instance id, the way the form editor writes them, and DB 19 repairs the forms an earlier version already converted - saved forms and trashed ones alike, since a trashed form can be restored. A form that was saved normally is left untouched, and a field used twice in the same form keeps both instances. * Feature: `uninstall.php`, honoring a new opt-in preference. The plugin had no uninstall handler at all - deleting it from the Plugins screen left every table and option behind, and only the Danger Zone button or the deactivation modal ever removed anything. Deleting a plugin is routinely a step in reinstalling or troubleshooting it, so the new `accua_forms_delete_data_on_uninstall` option ships **off** and the uninstaller then does nothing whatsoever. Ticking "Delete all Contact Forms data when the plugin is deleted" in the Danger Zone makes it run the same routine as the Danger Zone button: uploaded files at the configured destination (with the plugin-owned-directory guard), the three tables, every option including the captcha secret keys and the preference itself, the cron event, the transients and the per-user screen preferences - and nothing another plugin owns. Multisite is handled per site: the preference, the options and the tables all belong to a single site, so a network uninstall deletes the data of the sites that opted in and leaves the others exactly as they are. The deletion routine and its helpers live in `includes/data-deletion.php`, which defines functions and registers no hooks, because uninstall.php runs with the plugin not loaded and has to require what it uses itself. * Fix: an unreadable field list is no longer replaced by the shipped defaults on upgrade (found on a real site while testing this release). If the `accua_forms_avail_fields` option cannot be unserialized, `get_option()` returns false, `accua_forms_install()` reads that as "no fields defined yet" and writes the default field set over it - discarding every field the site had defined, with their labels and options, and no way back. It only bites on an upgrade, because the install routine runs when the stored DB version changes, which is exactly when such a site meets it. The usual cause is a migration or search-and-replace tool that rewrote the option text (classically normalizing CRLF to LF) without recomputing the `s::` headers PHP validates: the data is all there, only the lengths disagree. The upgrade now recomputes those lengths and accepts the result only if it unserializes into something shaped like a field list; either way the untouched original is copied to `accua_forms_avail_fields_corrupt_backup` before anything is written, so nothing is lost even when the repair does not work. A healthy option is never touched: each declared length is trusted first and only shortened when it fails to land on the closing quote. * Compatibility note for the DB 18 conversion: the binary collation is derived from the column's own charset (`latin1_bin`, `utf8mb3_bin`, `utf8mb4_bin` …), verified on real latin1 and legacy-utf8 installs, so a site that never moved to utf8mb4 converts correctly instead of failing on a hardcoded collation name. Only `afsv_field_id` changes; `afsv_value` and every other column keep theirs. Code that joins its own table against `afsv_field_id` keeps working without changes, whichever collation it uses on its side - a plugin that reads the live collation gains the case distinction, one that hardcodes a utf8mb4 collation simply keeps matching case-insensitively as before. A field slug still cannot be changed once the field exists: it is the key every form instance and every stored submission refers to, and anything else holding it - an extension, custom code, an external integration - could not be updated along with it. * Fix: field slugs are now case-sensitive throughout, and two fields whose slugs differ only in case no longer collide (user-reported from a 2.2.32 site). Field definitions live in a PHP array, where keys are case-sensitive, so a site could legitimately hold both a `role` text field and a `Role` checkbox - but the submission values table stored `afsv_field_id` with the site's case-insensitive collation, and to MySQL the two were the same string. The visible symptom was that the submissions list and the Excel export offered a single column for the pair, because both are built from `SELECT DISTINCT afsv_field_id`. The unreported and more serious one: the table's `PRIMARY KEY (afsv_sub_id, afsv_field_id)` also treated them as one, so on every submission the second field's value was rejected as a duplicate key and silently lost. DB version 18 converts that one column to the binary collation of its own charset (an explicit ALTER, the way WordPress core changes collations; dbDelta does not compare them). Converting in this direction cannot fail on existing data: values that were equal under the old collation could never both exist. Only that column changes, so its indexes keep working and no query needs a per-query COLLATE. * Fix: the Excel export ran the selected column keys through `sanitize_key()`, which lowercases. Column keys embed the field slug verbatim (`_field_{slug}`), so exporting a site with `role` and `Role` merged the two regardless of the database. The export now uses a case-preserving sanitizer (`accua_forms_sanitize_column_key()`) accepting the same characters a slug may contain. * Change: newly created field slugs are lowercased, the way WordPress lowercases term slugs. Existing slugs are never touched - a site already holding mixed-case slugs keeps them working, and they remain editable - but a new field can no longer accidentally introduce a `role`/`Role` pair. The Fields page input lowercases as you type, so it always shows what will be stored. * Change: reCAPTCHA v2 now defaults to "Reject with an error message" instead of the silent "Accept silently and mark as Spam" default introduced for both captcha types in 2.2.40 (user-reported). v2 shows the visitor a checkbox to solve, so a failed check is usually not a bot: it is a real person who did not tick it, whose token expired (Google keeps them valid for two minutes) or who retried with a token already consumed (they are single-use). Accepting those silently filed a genuine message under the Spam status and sent no notification email, with nothing on screen to tell the visitor. Google's guidance for a failed verification is to surface the error and let the visitor solve the challenge again, which is what this default now does - the AJAX handler already resets the widget after every response, so the retry carries a fresh token. reCAPTCHA v3 keeps the silent "mark as Spam" default: its score is computed invisibly, there is nothing for the visitor to solve, and a low score is best absorbed without bouncing anyone. * Implementation: the two types now have separate site-wide defaults in the "Captcha defaults" settings box - "When the reCAPTCHA v2 check fails" (new `captcha_spam_action_v2` option, shipped default `reject`) and "When the reCAPTCHA v3 check fails" (the existing `captcha_spam_action`, shipped default `spam`) - because the settings page writes its select on every save, so a shared key would have masked the new v2 default on any site that had ever saved the settings page. `accua_forms_captcha_default_spam_action()` and `accua_forms_captcha_spam_action()` take the field type as a new argument (defaulting to the v3 resolution, so pre-2.3.0 callers are unaffected); the per-field "override" checkbox in the form editor is unchanged, and fields that already carry an explicit `spam_action` keep it. * Fix: a form carrying both a v2 and a v3 field could apply the wrong action. The submission handler resolved one action per form by scanning for the first captcha field, which was harmless while both types shared a default but not once they differ. `AccuaForm_Validation_CaptchaSpam` now records the silent action of the validator that actually failed (`getSpamAction()`), and the handler follows that, so each captcha field's setting governs its own failures. * Fix: the "Contact Form" block rendered nothing on WordPress 5.9 and 6.0, which the plugin still declares support for. `block.json` describes its dynamic rendering with the `render` property, honored only from WordPress 6.1, and the block was registered without a `render_callback` fallback - so on older versions it registered fine, appeared in the inserter, and produced an empty front end. `register_block_type()` is now called with an explicit `render_callback`; on 6.1+ the explicit argument wins over the metadata-derived one and both resolve to the same `block-editor/render.php`, so nothing changes on current WordPress. * Public release consolidating the 2.2.33 - 2.2.47 development versions (the previously published version was 2.2.32). The readme.txt changelog entry for 2.3.0 summarizes all of them; the per-version detail stays in this file. * Plugin Check (PCP) 2.0.0 final compliance pass: fixed the one finding on the shipped file set - the Fields page live preview printed the JSON-encoded "this is a preview" note into its inline script without an escaping annotation. The value is wp_json_encode() output of an already-escaped string, safe in a script context, now annotated as such (admin/fields-page.php). No functional changes. * i18n: completed the one missing translation in the bundled catalogs (the "%d field deleted." plural, it_IT and es_ES) and regenerated POT/PO/MO. Both catalogs are now 100% translated. * Fix: the two high-contrast blocks in frontend.css were written as `@media (prefers-contrast: high)`. `high` is not a value in Media Queries Level 5 (`no-preference | more | less | custom`), so the query never matched in Chrome or Firefox and the styles - a bolder floating label in the inline-label layout, 2px borders and a focus outline on the post-select control - had never applied for anyone browsing with a contrast preference. Now `@media (prefers-contrast: more), (prefers-contrast: high)`, keeping the legacy value for older WebKit. CSS version 205. * Readme accuracy pass over the whole readme.txt, verified claim by claim against the code. Corrected: "20 available field types" (there are 24, and the list omitted Telephone, Date, Post select and Color picker); the "orange C icon" (the classic-editor button is the cyan Contact Forms logo, and the Gutenberg "Contact Form" block was not mentioned anywhere despite shipping since 2.0); a "Preview/Test Tab" that does not exist (the form editor has a live preview pane); the "Appearance" tab (labelled "Appearance and General"); an "Advanced Excel Export option" (there are two export buttons, and the frozen header row plus column filters apply to every export); Cloudflare Turnstile listed as if built in (it requires the free Simple Cloudflare Turnstile plugin); the spam-handling sentence, which read as if Spam, Trash and delete were all the default (only "mark as Spam" is); screenshot 1 calling Top Labels the default layout (the default is labels on the left); "ARIA labels" (labelling is native `label for`, the ARIA work is `aria-describedby`/`aria-invalid` and live regions); "all captcha fields" for Hide field title (Turnstile does not offer it); the reCAPTCHA v3 notice being printed "under the form" (it renders where the v3 field sits); "long values compress in every column" (URL and field columns); the dashboard "Go to:" links listing "each lead status" (only those in use); and the claim that extension field types show their own settings on the Fields page (they get the generic sections - that page fires no `accua_forms_field_settings` hook). * Added the missing `Requires PHP: 7.4` line to the plugin header, which previously declared it only in readme.txt. * Improved: the "Essential Columns" button tooltip explains where its list comes from (user-reported: the tooltip named the columns but read as a fixed, generic list). It already listed the columns actually configured on the site - the always-essential ones plus the flagged field columns - but nothing told the administrator that the field part is theirs to choose. A second line now says so, naming the option and the screen: "Choose which field columns are kept: edit the field on the Fields page and enable 'Show in essential columns'." Rendered as a real second line (title attributes honor newlines) and translated in it_IT and es_ES. The first line keeps its existing wording and translations. = 2.2.47 = * Feature: per-field "Show in essential columns" checkbox on the Fields page (add and edit form). `Accua_Forms_Submissions_List_Table::essential_columns()` is no longer a hardcoded list: Actions, ID, Form and Submitted are always essential, and a field column is included when its definition carries the new `essential_column` flag - the "Essential Columns" button and its tooltip follow automatically since both already consume `essential_columns()`. The previously hardcoded IP, Page, Referrer and Language main columns are no longer essential (diagnostic metadata, not what a submission list is scanned for). The flag is written explicitly (0/1) by `accua_forms_fields_filter_values()` - a missing key means "never saved on this version", which is what the upgrade migration keys on. The checkbox section is hidden for field types that never store a submission value (submit, html, the four captcha types); extension types get it like the other generic sections. It renders as the last option of the editor, behind an hr separator: it is an admin-list-only setting with no effect on how the field renders in forms, so it is kept visually apart from the settings above, which all shape the field itself. * Feature: sensible defaults for the flag - fresh installs mark first_name, last_name, email and message (who wrote, and what they wrote); the first-visit default hidden columns of the submissions list (`default_hidden_columns` filter) now follow the same flag instead of hardcoding the email field. * Upgrade: DB version 17. Existing installations get `essential_column = 1` on the email field only - exactly the field column of the old hardcoded list, so the button keeps showing the same field columns right after the update. Guarded by isset (the Fields page save always writes the key), so unchecking email later is never overridden by a later plugin activation run. Dev-stack note: bump the DB version constant last when editing a live-mounted install - the constant is compared on every init, so a request landing between the constant bump and the migration code burns the version with a no-op run (happened during development; recovered by resetting `accua_forms_db_version` and reloading). * Fix: the Fields page live preview frame appeared oversized and shrank to the right size a moment later (user-reported). `resizePreview()` ran right after `document.write()`, measuring the written document before its stylesheets had loaded - unstyled form content is taller, so the frame grew immediately and only settled when the CSS applied and the ResizeObserver/timed re-measures caught up. The first resize (and the ResizeObserver setup) now waits for the frame's load event - bound on the iframe element, which survives `document.open()`, and before `doc.close()` so it cannot be missed - with a 1.5s fallback in case a subresource hangs; the loading overlay stays on until then; superseded refreshes clear the pending settle/re-measure timers, and the outgoing document's observer is disconnected before the write so its detached nodes cannot trigger a measure of the unstyled new document. The frame now goes from its previous height straight to the final one. Follow-up polish in the same release (user request: nothing in the admin may jump or resize abruptly): the frame starts compact (48px - below the 80px measure floor, so the first render can only grow it) instead of at an oversized 200px placeholder; every height change animates (0.25s ease, disabled under prefers-reduced-motion); the heavy white veil + large spinner were replaced by a small WordPress-size spinner with the frame content kept fully transparent while loading - the white wrapper shows through, and since the reveal happens at the frame's load event a document still loading its stylesheets can never flash unstyled (the veil used to mask that FOUC; removing it exposed the flash until the opacity gate restored the guarantee); and the preview document is written with `html { overflow: hidden }` so no scrollbar flashes while the frame is shorter than its content or animating - the resize logic restores scrolling only when the content exceeds the 700px height cap. The form editor preview keeps its existing veil + spinner treatment. * Fix: the preview document is written with a `` - without one the iframe rendered in Quirks Mode (jQuery is explicitly unsupported there and layout metrics differ; jQuery Migrate warned about it on every preview render). * Improved: submitting the Fields page editor form cancels the pending debounced preview refresh and aborts an in-flight preview request - typing the label and clicking "Add new field" within the 700ms debounce window fired a refresh that raced the page unload for nothing. If the client-side validation blocks the submit, the next input schedules a fresh refresh as usual. * Verification: submissions-list.spec.ts grew from 14 to 15 tests - an "Essential Columns button" describe seeds one flagged and one unflagged E2E field with stored values and asserts the click keeps ID/Actions/Submitted and the flagged field column while hiding the unflagged field, IP and Page (the admin's saved hidden-columns screen option is snapshotted and restored); the tooltip test now asserts the Email label and the smaller column set. fields-page.spec.ts grew from 29 to 30 - checkbox visibility per type (shown for textfield and unknown extension types, hidden for captcha/submit/html) and a full round trip: create with the flag → stored as 1 and present in essential_columns(), edit shows it pre-checked, uncheck → stored as 0 and absent. Full suite green. A Chromium automation gotcha surfaced by the new test is documented in the workspace testing skill: after any form-POST navigation the Playwright-bundled Chromium stops servicing requestAnimationFrame for that tab (reproduced on core options-general.php - no plugin code involved), so actionability-gated clicks on pages after a POST need force: true; not a plugin bug. * i18n: 2 new strings translated in the bundled it_IT and es_ES catalogs (POT/PO/MO regenerated). * Release housekeeping: 2.2.46 was not published to wordpress.org either (the public version is still 2.2.32), so the consolidated readme.txt entry is renamed to 2.2.47 and now includes the essential-columns feature and the preview sizing fix. CSS version 204, JS version 132, DB version 17. = 2.2.46 = * Fix: the submissions list Page and Referrer columns lost their compression - long URLs stretched the columns to the full URL width instead of collapsing behind the [+] expandable widget. Root cause: the 2.2.39 switch to content-based column sizing dropped the core 'fixed' table class, and without `table-layout: fixed` the CSS truncation on the cells (overflow + text-overflow) cannot constrain a table column, so `expandable-cells.js` never measured an overflowing cell and never built the `
` widget. The still-published 2.2.32 predates that change and truncates fine, which is how the regression surfaced (production compressed, dev not). The URL columns now carry an explicit `max-width: 15em` in admin.css - honored by auto table layout in all browsers - so truncation and the [+] widget work again while every other column keeps sizing to its content. The dashboard "Last 10 submissions" table was never affected (it kept the fixed class and a colgroup). * Feature: "Expand All Rows" and "Collapse All Rows" buttons in the submissions list toolbar next to "Essential Columns" - icon-only buttons with the action name in the tooltip (`title`) and in `aria-label` for screen readers. No dashicon conveys "expand/collapse rows" (the editor-expand/contract pair reads as fullscreen), so the icons are two new plugin SVGs (`assets/img/expand-rows.svg` / `collapse-rows.svg`: a bulleted row list with vertical arrows pointing outward, respectively inward), applied via CSS `mask` with `background-color: currentColor` so they take the button text color in every state. The buttons are flex containers (`inline-flex` + `align-items: center`), so the icon is centered exactly regardless of the `.button` line-height/min-height metrics that differ between WordPress releases. They open/close every [+] expandable URL cell on the page at once (new `accuaToggleAllRows()` in submissions-list.js; the per-cell toggle listeners keep handling the row-height styles). * Improved: the "Essential Columns" button explains itself - its tooltip lists the columns it keeps, with the same localized labels the table shows ("Keep only these columns visible: ID, Actions, Form, IP, Page, Referrer, Language, Submitted, Email"), built from `get_columns()` and skipping keys absent on the site. The column-key list moved to a single source of truth (`Accua_Forms_Submissions_List_Table::essential_columns()`), mirrored to `setEssentialColumns()` in submissions-list.js via `wp_localize_script` instead of being duplicated there. * Fix: browser find-in-page (Ctrl+F) counted every match inside a compressed URL cell twice (user-reported; e.g. searching a word contained in a collapsed referrer). The [+] widget was a `
`/`` holding the text twice - a truncated preview in the summary plus the full link in the details body - and both Chrome and Firefox search collapsed `
` content (that is also what lets them auto-open one on a match), so both copies counted, collapsed or expanded. The widget is rebuilt around a single text copy: the link itself, CSS-truncated while collapsed (`.accua-expandable-text`), with a real `[+]`/`[−]` toggle `