Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@
- Fixed a bug where typing in a newly added nested entry’s fields could replace the edit form with a “UI array path … must be a row index” error once the entry autosaved.
- Fixed a bug where changing a field back to the value it had when the edit page loaded wasn’t autosaved, so field layout elements shown or hidden by conditions based on it didn’t update.
- Fixed a bug where Table fields’ Date and Time cells, and their default row values, shifted by the site’s UTC offset each time they were saved from the control panel.
- Fixed bugs where Dropdown fields didn’t preserve blank option values and could show duplicate blank placeholders. ([#20140](https://github.com/craftcms/cms/pull/20140))
- Fixed a bug where going from one element edit page to another, such as switching sites, opened the next page’s “Revisions” details panel rather than the one that was open.
- Fixed a bug where viewing an element revision showed three read-only notices, including one about settings changes not being permitted, and no “Revert content from this revision” button. ([#20115](https://github.com/craftcms/cms/pull/20115))
- Fixed an error that occurred on release pages if one of the release’s drafts belonged to an element that had been deleted, such as along with its section. The draft is now listed as blocked with “The entry has been deleted.”, and can be removed by anyone who can manage the release.
Expand Down
3 changes: 3 additions & 0 deletions src/Field/BaseOptionsField.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ public function __construct($config = [])
];
} else {
unset($option['isOptgroup']);
// Blank cells are posted as null (ConvertEmptyStringsToNull), and null keys don't survive saving
$option['label'] = (string) ($option['label'] ?? '');
$option['value'] = (string) ($option['value'] ?? '');
$options[] = $option;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/Field/Dropdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use CraftCms\Cms\Field\Data\OptionData;
use CraftCms\Cms\Field\Data\SingleOptionFieldData;
use CraftCms\Cms\Support\Facades\DeltaRegistry;
use CraftCms\Cms\Ui\Contracts\Control;
use CraftCms\Cms\Ui\Controls\Choice;

use function CraftCms\Cms\t;

Expand Down Expand Up @@ -62,6 +64,19 @@ public function getStatus(ElementInterface $element): ?array
return parent::getStatus($element);
}

#[\Override]
public function uiControl(FieldContext $context): Control
{
$control = parent::uiControl($context);

// A blank option is encoded as __blank__, so Choice can't tell it supplies its own empty state
if ($control instanceof Choice && collect($this->options)->contains(fn (array $option): bool => ($option['value'] ?? null) === '')) {
$control->withoutPlaceholder();
}

return $control;
}

#[\Override]
protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inline): string
{
Expand Down
58 changes: 58 additions & 0 deletions tests/Feature/Field/OptionsFieldEmptyValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

use CraftCms\Cms\Field\Dropdown;
use CraftCms\Cms\Field\FieldContext;
use CraftCms\Cms\Field\Fields;

test('an option saved without a value is treated as a blank option', function () {
$field = new Dropdown([
'options' => [
['label' => 'None', 'default' => ''],
['label' => 'A', 'value' => 'a', 'default' => ''],
],
]);

expect($field->options[0]['value'])->toBe('')
->and($field->normalizeValue(null, null)->value)->toBe('');
});

test('a blank option value posted as null is saved as an empty string', function () {
$fields = app(Fields::class);

$fields->saveField($field = $fields->createField([
'type' => Dropdown::class,
'name' => 'Dropdown',
'handle' => 'dropdown',
'options' => [
['label' => 'None', 'value' => null, 'default' => ''],
['label' => 'A', 'value' => 'a', 'default' => ''],
],
]));

$fields->refreshFields();

expect($fields->getFieldById($field->id)->getSettings()['options'][0]['value'])->toBe('');
});

test('a dropdown with its own blank option does not get another one', function () {
$field = new Dropdown([
'options' => [
['label' => 'Please select', 'value' => '', 'default' => ''],
['label' => 'A', 'value' => 'a', 'default' => ''],
],
]);

expect($field->uiControl(new FieldContext('value'))->props()['placeholder'])->toBeFalse();
});

test('a dropdown without a blank option keeps the leading blank', function () {
$field = new Dropdown([
'options' => [
['label' => 'A', 'value' => 'a', 'default' => ''],
],
]);

expect($field->uiControl(new FieldContext('value'))->props())->not->toHaveKey('placeholder');
});
Loading