You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Using the form builder's "Bulk Edit Options" on a radio/checkbox/select (or Likert, via frm_bulk_edit_field_types) field, a blank line in the textarea (or a label| line with nothing after the separator in separate-value mode) became an option with an empty string value. FrmAppHelper::check_selected() compares the field's current value against each option's value with a loose ==, so when the field has no submitted value yet (''), that blank option matches and renders checked/selected by default.
Related: Strategy11/formidable-pro#3385
What changed
FrmFieldsController::import_options() drops blank lines via parse_bulk_edit_opts(), and blank-value separate-value halves (label|) via remove_blank_separated_values() — except a select field's leading blank line, which is a legitimate, renderer-supported "please select" default (dropdown-field.php's own placeholder/skip handling) rather than the check_selected() collision this PR fixes. Behavior change for dropdowns: re-saving Bulk Edit Options on an existing select field no longer drops a leading blank option it already had.
A blank-label half (|value) is left alone: check_selected() never compares the label, so it doesn't reproduce the bug, and dropdown-field.php renders a blank-label option deliberately.
Mirrors the equivalent blank-line filtering formidable-pro's FrmProFieldProduct bulk edit for Product fields already does.
How it was verified
Red/green locally against the real PHPUnit suite (~/Claude/test-sites/formidable/wordpress-develop): unit tests for both private helpers (blank-line dropping, '0' survives, select keeps a leading blank/radio drops it, blank-value separate halves drop, blank-label halves survive) plus new integration tests driving the real frm_import_options AJAX action end-to-end (tests/phpunit/fields/test_FrmFieldsAjax.php) covering the label|value split ordering and the other_* key surviving the reindex. Full fields (114) and ajax (9) groups green afterward (one pre-existing, unrelated failure in test_FrmFieldCombo::test_print_input_atts, a PHP 8.5 deprecation notice leaking into output buffering, present before this change too).
Self-reviewed (security + correctness/simplify lenses) before pushing — security clean; correctness caught that the blank-label drop I'd added wasn't justified (reverted, see above).
We reviewed changes in 2a2d8c1...911bb43 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
The reason will be displayed to describe this comment to others. Learn more.
Request Changes.
The collision itself is real and correctly diagnosed — FrmAppHelper::check_selected() compares with a loose ==, so an option whose value is '' matches an unset field value and renders pre-selected. The '0' test is the right guard to have written; a bare array_filter() here would have silently eaten a legitimate zero option, and array_values() keeps the reindex from colliding with the other_* string keys merged in at :387. Two blocking items.
Blocking
PHPCS is red, and all three errors are PR-introduced and auto-fixable ([x]) — FrmFieldsController.php:414, test_FrmFieldsController.php:59 and :96. phpcbf clears all three.
The filter runs for every bulk-edit type, but a leading blank option is legitimate and renderer-supported on select. Opening Bulk Edit on such a dropdown and saving now silently drops it, even with no other edit. FrmFieldsController.php:354
Non-blocking
remove_blank_separated_values() only inspects the value half, so a |value line survives as an option with an empty label. FrmFieldsController.php:429
Both tests call the new private helpers directly, so import_options()'s own wiring — the ordering against the label|value split, and the interaction with the other_* merge — is still uncovered. test_FrmFieldsController.php:42
CI otherwise green (PHPUnit on PHP 7.4 and 8, PHP CS Fixer, Rector, ESLint, Oxlint, Stylelint, both syntax legs). Branch is current with master (0 behind). Source review only — no visual surface in the diff itself, though finding 2 is about rendered output and is source-derived rather than browser-confirmed ([Likely], from dropdown-field.php and add_placeholder_to_select()).
The reason will be displayed to describe this comment to others. Learn more.
Blocking: PHPCS is red, and all three errors come from this diff.
classes/controllers/FrmFieldsController.php
414 | ERROR | [x] Unnecessary blank line in short function with only 2 ...
tests/phpunit/fields/test_FrmFieldsController.php
59 | ERROR | [x] Missing docblock with @param tags (detected types from call ...
96 | ERROR | [x] Missing docblock with @param tags (detected types from call ...
:414 is the blank line between the array_map() assignment and the return in parse_bulk_edit_opts(). :59 and :96 are the two new private test helpers. All three are marked [x], so phpcbf fixes them — but confirm what it does to :414 rather than accepting it blind, since collapsing that function is a readability call, not just whitespace.
Worth noting the repo's PHPCS job lints the whole tree, so a red check here doesn't always mean the diff — this time it does. Nothing else in the file or the suite is flagged.
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Also: parse_bulk_edit_opts() grew past the short-function threshold once it took the select-type branch, so PHPCS no longer flags the blank line at all on the current diff (confirmed clean, bare phpcs, no --standard override).
The reason will be displayed to describe this comment to others. Learn more.
Blocking: this drops blank options for every bulk-edit type, but the defect is specific to the types where a blank option is never wanted. On select a leading blank option is a supported choice, not a mistake.
The blank option is skipped only when a placeholder is set — and it's skipped precisely because add_placeholder_to_select() (FrmFieldsController:920) already emitted its own <option value=""> and the renderer is avoiding a duplicate. With no placeholder configured, add_placeholder_to_select() returns false without emitting anything, the guard above doesn't fire, and :92 renders the blank option deliberately:
echoesc_html( $opt === '' ? '' : $opt );
That is the "nothing selected yet" entry on a non-required dropdown with no placeholder. Without it the browser auto-selects the first real option, which is the opposite of what the field author wanted. For a radio or checkbox group there is no equivalent use — a blank choice there is exactly the check_selected() collision this PR is fixing.
The regression doesn't need anyone to type a blank line, either. The Bulk Edit textarea is populated from the field's existing options, so opening Bulk Edit on a dropdown that already has a leading blank option and saving with no changes now silently removes it.
Scope the filtering to where the collision is actually a defect. $bulk_edit_types is already resolved a few lines up at :345, so the type is in hand:
with parse_bulk_edit_opts() keeping a single leading blank for select and dropping blanks everywhere else. If you'd rather not branch on type, the alternative is to drop only duplicate and trailing blanks and keep at most one leading blank — but the type check is the more honest statement of the rule.
Either way this needs saying in the PR description: as written it's a behavior change for dropdowns, not only a fix.
The reason will be displayed to describe this comment to others. Learn more.
Fixed as suggested: parse_bulk_edit_opts() now takes the field type and keeps a single leading blank for select only, dropping elsewhere. Added regression tests (select keeps it, radio drops it), both confirmed red against the prior code. Noted the dropdown behavior change in the PR description.
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking: the predicate only inspects the value half, so the mirror-image malformed line survives.
|value splits to array( 'label' => '', 'value' => 'value' ). '' !== $opt['value'] is true, so it's kept, and the option renders with an empty label against a real value. That isn't the check_selected() collision — the value is non-blank, so nothing pre-selects — but it's the same shape of malformed input arriving through the same split, and the docblock above describes the helper as handling that split generally.
Deciding to keep it is fine; it just isn't stated anywhere. A line in the docblock saying only the value half is checked, and why, would stop the next reader assuming both halves are covered.
Also: explode( '|', $opt ) at :362 keeps only $vals[0] and $vals[1], so a|b|c silently discards c. Pre-existing, not yours, and not worth widening this PR for — noting it because it's in the block you're now filtering.
The reason will be displayed to describe this comment to others. Learn more.
Kept, per your note - and now documented in the docblock why: check_selected() only ever compares the value half, and dropdown-field.php renders a blank-label option deliberately, so a value line survives on purpose rather than by omission. Added a test locking in that a blank-label/real-value line survives.
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking: both tests exercise the helpers in isolation, so import_options()'s own wiring is still uncovered.
parse_bulk_edit_opts() and remove_blank_separated_values() are each called directly through run_private_method(). That verifies the two predicates, which is the right start, but it means nothing covers the parts most likely to break later:
remove_blank_separated_values() runs after the label|value loop at :360-370, so it sees a mix of arrays and plain strings. The unit test hands it that mix by hand; nothing proves import_options() actually produces it in that order.
parse_bulk_edit_opts() now returns array_values()-reindexed keys, which then meet the other_* string keys in array_merge( $opts, $other_array ) at :387. The reindex is correct, but that interaction is exactly what a later refactor would break silently.
One test through import_options() with $_POST['opts'] containing a blank line, separate=true, and a field that has an other option would cover all of it, and would have caught either wiring mistake.
The PR description says the three helper tests were confirmed red against the old code — that's the right discipline and worth keeping for this one too.
The reason will be displayed to describe this comment to others. Learn more.
Added integration tests driving the real frm_import_options AJAX action end to end (test_FrmFieldsAjax.php): blank-line dropping, the select leading-blank case, the label|value split ordering, and the other_ key surviving the reindex/merge.
❌ Patch coverage is 85.71429% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.54%. Comparing base (d0ffb93) to head (9c598b9). ⚠️ Report is 813 commits behind head on master.
…alues, add wiring coverage
Franky round-1 review: parse_bulk_edit_opts() dropped a select field's
legitimate leading blank option (dropdown-field.php's own placeholder/skip
handling treats it as a manual "please select" default) along with genuine
bugs on radio/checkbox. Now preserves a single leading blank only for
select. remove_blank_separated_values() also drops a blank-label half
(mirroring the existing blank-value check) and import_options() itself
gets integration coverage via the real frm_import_options AJAX action,
not just the private helpers in isolation.
Self-review caught this: FrmAppHelper::check_selected() only ever
compares an option's value half, never its label, so a blank label with
a real value doesn't reproduce #3385's collision - and dropdown-field.php
already renders a blank label as a real, selectable option. Reverted
that part of the previous commit; kept the value-half check and the
select leading-blank preservation. Also drops the now-dead isset() check
in parse_bulk_edit_opts() (explode() always returns at least one element).
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
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.
What was broken
Using the form builder's "Bulk Edit Options" on a radio/checkbox/select (or Likert, via
frm_bulk_edit_field_types) field, a blank line in the textarea (or alabel|line with nothing after the separator in separate-value mode) became an option with an empty string value.FrmAppHelper::check_selected()compares the field's current value against each option's value with a loose==, so when the field has no submitted value yet (''), that blank option matches and renders checked/selected by default.Related: Strategy11/formidable-pro#3385
What changed
FrmFieldsController::import_options()drops blank lines viaparse_bulk_edit_opts(), and blank-value separate-value halves (label|) viaremove_blank_separated_values()— except a select field's leading blank line, which is a legitimate, renderer-supported "please select" default (dropdown-field.php's own placeholder/skip handling) rather than thecheck_selected()collision this PR fixes. Behavior change for dropdowns: re-saving Bulk Edit Options on an existingselectfield no longer drops a leading blank option it already had.|value) is left alone:check_selected()never compares the label, so it doesn't reproduce the bug, anddropdown-field.phprenders a blank-label option deliberately.FrmProFieldProductbulk edit for Product fields already does.How it was verified
Red/green locally against the real PHPUnit suite (
~/Claude/test-sites/formidable/wordpress-develop): unit tests for both private helpers (blank-line dropping,'0'survives, select keeps a leading blank/radio drops it, blank-value separate halves drop, blank-label halves survive) plus new integration tests driving the realfrm_import_optionsAJAX action end-to-end (tests/phpunit/fields/test_FrmFieldsAjax.php) covering the label|value split ordering and theother_*key surviving the reindex. Fullfields(114) andajax(9) groups green afterward (one pre-existing, unrelated failure intest_FrmFieldCombo::test_print_input_atts, a PHP 8.5 deprecation notice leaking into output buffering, present before this change too).Self-reviewed (security + correctness/simplify lenses) before pushing — security clean; correctness caught that the blank-label drop I'd added wasn't justified (reverted, see above).