Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting minimum and maximum count of selected #96

Merged
merged 12 commits into from
Oct 30, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* (feature) Extract parsing of `_editable` data into `PreviewDataParser`.
* (internal) Expose `_editable` data via `StoryMetaData::getPreviewData()`.
* (feature) Add `ComponentPreviewData` helper to easily render component meta and preview data.
* (improvement) Add support for setting minimum and maximum count of selected `ChoiceField` options.


3.2.2
Expand Down
128 changes: 115 additions & 13 deletions src/Field/Definition/ChoiceField.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace Torr\Storyblok\Field\Definition;

use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\Count;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Type;
use Torr\Storyblok\Context\ComponentContext;
use Torr\Storyblok\Exception\InvalidFieldConfigurationException;
use Torr\Storyblok\Field\Choices\ChoicesInterface;
use Torr\Storyblok\Field\FieldType;
use Torr\Storyblok\Visitor\DataVisitorInterface;
Expand All @@ -20,9 +22,29 @@ public function __construct (
private readonly ChoicesInterface $choices,
private readonly bool $allowMultiselect = false,
private readonly int|string|\BackedEnum|null $defaultValue = null,
private readonly ?int $minimumNumberOfOptions = null,
private readonly ?int $maximumNumberOfOptions = null,
)
{
parent::__construct($label, $this->defaultValue);

if (!$this->allowMultiselect && (null !== $this->minimumNumberOfOptions || null !== $this->maximumNumberOfOptions))
{
throw new InvalidFieldConfigurationException(\sprintf(
"Can't configure minimum or maximum amount of options for single-select choice.",
));
}

if (
null !== $this->minimumNumberOfOptions
&& null !== $this->maximumNumberOfOptions
&& $this->minimumNumberOfOptions > $this->maximumNumberOfOptions
)
{
throw new InvalidFieldConfigurationException(\sprintf(
"The minimum number of options value can't be higher than the maximum",
));
}
}


Expand All @@ -48,41 +70,121 @@ protected function toManagementApiData () : array
"default_value" => $this->defaultValue instanceof \BackedEnum
? $this->defaultValue->value
: $this->defaultValue,
"min_options" => $this->minimumNumberOfOptions,
"max_options" => $this->maximumNumberOfOptions,
],
);
}

/**
* @inheritDoc
*
* @param string|int|null|array<string|int|null> $data
*/
public function validateData (ComponentContext $context, array $contentPath, mixed $data, array $fullData) : void
{
$allowedValueTypeConstraints = new AtLeastOneOf([
new Type("string"),
new Type("int"),
]);
if ($this->allowMultiselect)
{
\assert(null === $data || \is_array($data));
keichinger marked this conversation as resolved.
Show resolved Hide resolved

$this->validateMultiSelect($context, $contentPath, $data);
}
else
{
\assert(null === $data || \is_string($data) || \is_int($data));

$this->validateSingleSelect($context, $contentPath, $data);
}
}


private function validateSingleSelect (ComponentContext $context, array $contentPath, string|int|null $data) : void
{
$data = $context->normalizeOptionalString((string) $data);

if (null === $data)
{
return;
}

\assert(\is_string($data));
keichinger marked this conversation as resolved.
Show resolved Hide resolved

$context->ensureDataIsValid(
$contentPath,
$this,
$data,
[
$this->allowMultiselect
? new All([new NotNull(), $allowedValueTypeConstraints])
: $allowedValueTypeConstraints,
new Type("string"),
$this->required
? new NotBlank()
keichinger marked this conversation as resolved.
Show resolved Hide resolved
: null,
],
);

\assert(null === $data || \is_array($data) || \is_int($data) || \is_string($data));
$choicesConstraints = $this->choices->getValidationConstraints(false);

if (\is_string($data))
if (!empty($choicesConstraints))
{
$data = $context->normalizeOptionalString($data);
$context->ensureDataIsValid(
$contentPath,
$this,
$data,
$choicesConstraints,
);
}
}


/**
* @param array<int|string|null>|null $data
*/
private function validateMultiSelect (ComponentContext $context, array $contentPath, array|null $data) : void
{
if (null === $data)
{
return;
}

\assert(\is_array($data));

$data = \array_map(
static fn (mixed $value) => $context->normalizeOptionalString((string) $value),
$data,
);

$context->ensureDataIsValid(
$contentPath,
$this,
$data,
[
new Type("array"),
keichinger marked this conversation as resolved.
Show resolved Hide resolved
new All([
new NotNull(),
new Type("string"),
]),
],
);

if ($this->required || null !== $this->minimumNumberOfOptions || null !== $this->maximumNumberOfOptions)
{
$context->ensureDataIsValid(
$contentPath,
$this,
$data,
[
new Count(
min: $this->minimumNumberOfOptions ?? ($this->required ? 1 : null),
max: $this->maximumNumberOfOptions,
minMessage: "At least {{ limit }} option(s) must be selected.",
maxMessage: "You cannot specify more than {{ limit }} options.",
),
],
);
}

$choicesConstraints = $this->choices->getValidationConstraints($this->allowMultiselect);
$choicesConstraints = $this->choices->getValidationConstraints(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I see that: I really dislike this API. We need to change it in the next major version 😅


if (null !== $data && !empty($choicesConstraints))
if (!empty($choicesConstraints))
{
$context->ensureDataIsValid(
$contentPath,
Expand Down
Loading