diff --git a/assets/js/app/editor/Components/Select.vue b/assets/js/app/editor/Components/Select.vue index 2f1b85984..31671c62d 100644 --- a/assets/js/app/editor/Components/Select.vue +++ b/assets/js/app/editor/Components/Select.vue @@ -3,7 +3,6 @@ setCacheKey([$contentTypeSlug, $order, $format, (string) $required, $maxAmount]); - return $this->execute([parent::class, __FUNCTION__], [$contentTypeSlug, $order, $format, $required, $maxAmount]); + return $this->execute([parent::class, __FUNCTION__], [$contentTypeSlug, $order, $format, $required, $allowEmpty, $maxAmount]); } } diff --git a/src/Entity/Field.php b/src/Entity/Field.php index d1413cd2e..d9ebfb74f 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -16,7 +16,7 @@ use Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\SerializedName; -use Tightenco\Collect\Support\Collection as LaravelCollection; +use Tightenco\Collect\Support\Collection; use Twig\Environment; use Twig\Markup; @@ -155,7 +155,7 @@ private function setDefinitionFromContentDefinition(): void } } - public function setDefinition($name, LaravelCollection $definition): void + public function setDefinition($name, Collection $definition): void { $this->fieldTypeDefinition = FieldType::mock($name, $definition); } @@ -177,7 +177,7 @@ public function get($key) $default = $this->getDefaultValue(); if ($this->isNew() && $default !== null) { - if (! $default instanceof LaravelCollection) { + if (! $default instanceof Collection) { throw new \RuntimeException('Default value of field ' . $this->getName() . ' is ' . gettype($default) . ' but it should be an array.'); } @@ -439,4 +439,39 @@ public static function setTwig(Environment $twig): void { self::$twig = $twig; } + + public function allowEmpty(): bool + { + return self::definitionAllowsEmpty($this->getDefinition()); + } + + public static function definitionAllowsEmpty(Collection $definition): bool + { + return self::settingsAllowEmpty( + $definition->get('allow_empty', null), + $definition->get('required', null) + ); + } + + /** + * True if settings allow empty value. + * + * Settings priority: + * - allow_empty + * - required + * + * Defaults to true. + */ + public static function settingsAllowEmpty(?bool $allowEmpty, ?bool $required): bool + { + if (!is_null($allowEmpty)) { + return boolval($allowEmpty); + } + + if (!is_null($required)) { + return !boolval($required); + } + + return true; + } } diff --git a/src/Entity/Field/SelectField.php b/src/Entity/Field/SelectField.php index 8a6f61b59..ccded5f76 100644 --- a/src/Entity/Field/SelectField.php +++ b/src/Entity/Field/SelectField.php @@ -44,7 +44,7 @@ public function getValue(): ?array { $value = parent::getValue(); - if (empty($value) && $this->getDefinition()->get('required')) { + if (empty($value) && !$this->allowEmpty()) { $value = $this->getDefinition()->get('values'); // Pick the first key from Collection, or the full value as string, like `entries/id,title` diff --git a/src/Twig/ContentExtension.php b/src/Twig/ContentExtension.php index bb3b2d9c5..881f71819 100644 --- a/src/Twig/ContentExtension.php +++ b/src/Twig/ContentExtension.php @@ -507,7 +507,7 @@ public function taxonomyOptions(Collection $taxonomy): Collection // We need to add this as a 'dummy' option for when the user is allowed // not to pick an option. This is needed, because otherwise the `select` // would default to the first option. - if ($taxonomy['required'] === false) { + if (Field::definitionAllowsEmpty($taxonomy)) { $options[] = [ 'key' => '', 'value' => '', @@ -549,7 +549,7 @@ public function taxonomyValues(\Doctrine\Common\Collections\Collection $current, $orders = $orders[$taxonomy['slug']] ?? []; } - if (empty($values) && $taxonomy['required']) { + if (empty($values) && !Field::definitionAllowsEmpty($taxonomy)) { $values[] = key($taxonomy['options']); $orders = array_fill(0, count($values), 0); } diff --git a/src/Twig/FieldExtension.php b/src/Twig/FieldExtension.php index 775794101..b0b0ba78b 100644 --- a/src/Twig/FieldExtension.php +++ b/src/Twig/FieldExtension.php @@ -169,7 +169,7 @@ public function getListTemplates(TemplateselectField $field): Collection $options = []; - if ($definition->get('required') === false) { + if ($field->allowEmpty()) { $options = [[ 'key' => '', 'value' => '(choose a template)', @@ -227,7 +227,7 @@ private function selectOptionsArray(Field $field): Collection // We need to add this as a 'dummy' option for when the user is allowed // not to pick an option. This is needed, because otherwise the `select` // would default to the one. - if (! $field->getDefinition()->get('required', true)) { + if ($field->allowEmpty()) { $options[] = [ 'key' => '', 'value' => '', @@ -259,7 +259,7 @@ private function selectOptionsContentType(Field $field): Collection // We need to add this as a 'dummy' option for when the user is allowed // not to pick an option. This is needed, because otherwise the `select` // would default to the one. - if (! $field->getDefinition()->get('required', true)) { + if ($field->allowEmpty()) { $options[] = [ 'key' => '', 'value' => '', @@ -307,4 +307,4 @@ public function selectOptionsHelper(string $contentTypeSlug, array $params, Fiel return $options; } -} \ No newline at end of file +} diff --git a/src/Twig/RelatedExtension.php b/src/Twig/RelatedExtension.php index 17ffafa41..230bf1e34 100644 --- a/src/Twig/RelatedExtension.php +++ b/src/Twig/RelatedExtension.php @@ -138,7 +138,7 @@ private function extractContentFromRelation(Relation $relation, Content $source) return null; } - public function getRelatedOptions(string $contentTypeSlug, ?string $order = null, string $format = '', ?bool $required = false): Collection + public function getRelatedOptions(string $contentTypeSlug, ?string $order = null, string $format = '', ?bool $required = false, ?bool $allowEmpty = false): Collection { $maxAmount = $this->config->get('maximum_listing_select', 1000); @@ -148,7 +148,7 @@ public function getRelatedOptions(string $contentTypeSlug, ?string $order = null $order = $contentType->get('order'); } - $options = $this->optionsUtility->fetchRelatedOptions($contentTypeSlug, $order, $format, $required, $maxAmount); + $options = $this->optionsUtility->fetchRelatedOptions($contentTypeSlug, $order, $format, $required, $allowEmpty, $maxAmount); return new Collection($options); } diff --git a/src/Utils/RelatedOptionsUtility.php b/src/Utils/RelatedOptionsUtility.php index c3c331eb2..d1a032ace 100644 --- a/src/Utils/RelatedOptionsUtility.php +++ b/src/Utils/RelatedOptionsUtility.php @@ -3,6 +3,7 @@ namespace Bolt\Utils; use Bolt\Entity\Content; +use Bolt\Entity\Field; use Bolt\Storage\Query; /** @@ -27,7 +28,7 @@ public function __construct(Query $query, ContentHelper $contentHelper) /** * Decorated by `Bolt\Cache\RelatedOptionsUtilityCacher` */ - public function fetchRelatedOptions(string $contentTypeSlug, string $order, string $format, bool $required, int $maxAmount): array + public function fetchRelatedOptions(string $contentTypeSlug, string $order, string $format, bool $required, ?bool $allowEmpty, int $maxAmount): array { $pager = $this->query->getContent($contentTypeSlug, ['order' => $order]) ->setMaxPerPage($maxAmount) @@ -40,7 +41,7 @@ public function fetchRelatedOptions(string $contentTypeSlug, string $order, stri // We need to add this as a 'dummy' option for when the user is allowed // not to pick an option. This is needed, because otherwise the `select` // would default to the first one. - if ($required === false) { + if (Field::settingsAllowEmpty($allowEmpty, $required)) { $options[] = [ 'key' => '', 'value' => '', diff --git a/templates/_partials/fields/select.html.twig b/templates/_partials/fields/select.html.twig index 4639e3a48..eed4da00e 100644 --- a/templates/_partials/fields/select.html.twig +++ b/templates/_partials/fields/select.html.twig @@ -36,6 +36,5 @@ :autocomplete="{{ autocomplete }}" :readonly="{{ readonly|json_encode }}" :errormessage='{{ errormessage|json_encode }}' - :allowempty="{{ required ? 'false' : 'true' }}" > {% endblock %} diff --git a/templates/content/_relationships.html.twig b/templates/content/_relationships.html.twig index fa4a9ccff..8fbeebbd6 100644 --- a/templates/content/_relationships.html.twig +++ b/templates/content/_relationships.html.twig @@ -2,7 +2,8 @@ {% for contentType, relation in record.definition.relations %} - {% set options = related_options(contentType, relation.order|default(), relation.format|default(), relation.required) %} + {% set options = related_options(contentType, relation.order|default(), relation.format|default(), relation.required, relation.allow_empty) %} + {% set value = record|related_values(contentType) %}
@@ -26,7 +27,6 @@ :options="{{ options }}" :multiple="{{ relation.multiple ? 'true' : 'false' }}" :taggable=false - :allowempty="{{ relation.required ? 'false' : 'true' }}" :searchable=true >
diff --git a/templates/content/_taxonomies.html.twig b/templates/content/_taxonomies.html.twig index 35827ce4d..53e8cc85b 100644 --- a/templates/content/_taxonomies.html.twig +++ b/templates/content/_taxonomies.html.twig @@ -39,7 +39,6 @@ :options="{{ options }}" :multiple="{{ definition.multiple ? 'true' : 'false' }}" :taggable="{{ (definition.behaves_like == 'tags') ? 'true' : 'false' }}" - :allowempty="{{ definition.required ? 'false' : 'true' }}" > {% if definition.has_sortorder %}