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

[5.6] Add multi select element condition rule #15855

Open
wants to merge 5 commits into
base: 5.6
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Dropdown cells within editable tables are no longer center-aligned. ([#15742](https://github.com/craftcms/cms/issues/15742))
- Link fields marked as translatable now swap the selected element with the localized version when their value is getting propagated to a new site for a freshly-created element. ([#15821](https://github.com/craftcms/cms/issues/15821))
- Pressing <kbd>Return</kbd> when an inline-editable field is focused now submits the inline form. (Previously <kbd>Ctrl</kbd>/<kbd>Command</kbd> had to be pressed as well.) ([#15841](https://github.com/craftcms/cms/issues/15841))
- Entry conditions can now have an “Entries” rule.

### Accessibility
- Improved the control panel for screen readers. ([#15665](https://github.com/craftcms/cms/pull/15665))
Expand All @@ -25,7 +26,9 @@
- Added support for passing aliased field handles into element queries’ `select()`/`addSelect()` methods. ([#15827](https://github.com/craftcms/cms/issues/15827))

### Extensibility
- Added `craft\base\conditions\BaseElementsSelectConditionRule`.
- Added `craft\base\RequestTrait::getIsWebRequest()`. ([#15690](https://github.com/craftcms/cms/pull/15690))
- Added `craft\elements\condition\EntriesConditionRule`.
- Added `craft\events\DefineAddressCountriesEvent`. ([#15711](https://github.com/craftcms/cms/pull/15711))
- Added `craft\filters\BasicHttpAuthLogin`. ([#15720](https://github.com/craftcms/cms/pull/15720))
- Added `craft\filters\BasicHttpAuthStatic`. ([#15720](https://github.com/craftcms/cms/pull/15720))
Expand Down
237 changes: 237 additions & 0 deletions src/base/conditions/BaseElementsSelectConditionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<?php

namespace craft\base\conditions;

use Craft;
use craft\base\ElementInterface;
use craft\elements\conditions\ElementCondition;
use craft\elements\conditions\ElementConditionInterface;
use craft\helpers\App;
use craft\helpers\Cp;
use stdClass;
use yii\base\Exception;

/**
* BaseElementsSelectConditionRule provides a base implementation for element query condition rules that are composed of an multi-element select input.
*
* @property int|null $elementId
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.5.0
*/
abstract class BaseElementsSelectConditionRule extends BaseConditionRule
{
/**
* @var string|array|null
* @see getElementIds()
* @see setElementIds()
*/
private string|array|null $_elementIds = null;

/**
* @inheritdoc
*/
public string $operator = self::OPERATOR_IN;

/**
* Returns the element type that can be selected.
*
* @return string
*/
abstract protected function elementType(): string;

/**
* Returns the element source(s) that the element can be selected from.
*
* @return array|null
*/
protected function sources(): ?array
{
return null;
}

/**
* Returns the element condition that filters which elements can be selected.
*
* @return ElementConditionInterface|null
*/
protected function selectionCondition(): ?ElementConditionInterface
{
return null;
}

/**
* Returns the criteria that determines which elements can be selected.
*
* @return array|null
*/
protected function criteria(): ?array
{
return null;
}

/**
* @inheritdoc
*/
protected function operators(): array
{
return array_merge(parent::operators(), [
self::OPERATOR_IN,
self::OPERATOR_NOT_IN,
]);
}

/**
* @param bool $parse Whether to parse the value for an environment variable
* @return array|string|null
* @throws Exception
* @throws \Throwable
*/
public function getElementIds(bool $parse = true): array|string|null
{
if ($parse && is_string($this->_elementIds)) {
$elementId = App::parseEnv($this->_elementIds);
if ($this->condition instanceof ElementCondition && isset($this->condition->referenceElement)) {
$referenceElement = $this->condition->referenceElement;
} else {
$referenceElement = new stdClass();
}

$elementIds = Craft::$app->getView()->renderObjectTemplate($elementId, $referenceElement);

if (str_contains($elementIds, ',')) {
$elementIds = explode(',', $elementIds);
}

return $elementIds;
}

return $this->_elementIds;
}

/**
* @param array|string|null $elementIds
* @phpstan-param array<int|string>|string|null $elementIds
*/
public function setElementIds(array|string|null $elementIds): void
{
$this->_elementIds = $elementIds ?: null;
}

/**
* @inheritdoc
*/
public function getConfig(): array
{
return array_merge(parent::getConfig(), [
'elementIds' => $this->getElementIds(false),
]);
}

/**
* @inheritdoc
*/
protected function inputHtml(): string
{
if ($this->getCondition()->forProjectConfig) {
return Cp::autosuggestFieldHtml([
'suggestEnvVars' => true,
'suggestionFilter' => fn($value) => is_string($value) && strlen($value) > 0,
'required' => true,
'id' => 'elementIds',
'class' => 'code',
'name' => 'elementIds',
'value' => $this->getElementIds(false),
'tip' => Craft::t('app', 'This can be set to an environment variable, or a Twig template that outputs a comma separated list of IDs.'),
'placeholder' => Craft::t('app', '{type} IDs', [
'type' => $this->elementType()::displayName(),
]),
]);
}

$elements = $this->_elements();

return Cp::elementSelectHtml([
'name' => 'elementIds',
'elements' => $elements ?: [],
'elementType' => $this->elementType(),
'sources' => $this->sources(),
'criteria' => $this->criteria(),
'condition' => $this->selectionCondition(),
'single' => false,
]);
}

/**
* @return ElementInterface[]|null
* @throws Exception
* @throws \Throwable
*/
private function _elements(): ?array
{
$elementIds = $this->getElementIds();
if (!$elementIds) {
return null;
}

/** @var string|ElementInterface $elementType */
/** @phpstan-var class-string<ElementInterface>|ElementInterface $elementType */
$elementType = $this->elementType();
return $elementType::find()
->id($elementIds)
->status(null)
->all();
}

/**
* @inheritdoc
*/
protected function defineRules(): array
{
$rules = parent::defineRules();
$rules[] = [['elementIds'], 'safe'];
return $rules;
}

/**
* Returns whether the condition rule matches the given value.
*
* @param ElementInterface|int|array|null $value
* @return bool
* @throws Exception
* @throws \Throwable
*/
protected function matchValue(mixed $value): bool
{
$elementIds = $this->getElementIds();

if (!$elementIds) {
return true;
}

if (!$value) {
return false;
}

if ($value instanceof ElementInterface) {
$value = [$value->id];
} elseif (is_numeric($value)) {
$value = [(int)$value];
} elseif (is_array($value)) {
$values = [];
foreach ($value as $val) {
if ($val instanceof ElementInterface) {
$values[] = $val->id;
} elseif (is_numeric($val)) {
$values[] = (int)$val;
}
}
$value = $values;
}

return match ($this->operator) {
self::OPERATOR_IN => !empty(array_intersect($value, $elementIds)),
self::OPERATOR_NOT_IN => empty(array_intersect($value, $elementIds)),
default => false,
};
}
}
68 changes: 68 additions & 0 deletions src/elements/conditions/entries/EntriesConditionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace craft\elements\conditions\entries;

use Craft;
use craft\base\conditions\BaseElementsSelectConditionRule;
use craft\base\ElementInterface;
use craft\elements\conditions\ElementConditionRuleInterface;
use craft\elements\db\ElementQueryInterface;
use craft\elements\db\EntryQuery;
use craft\elements\Entry;
use craft\helpers\ArrayHelper;

/**
* Entries condition rule.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.5.0
*/
class EntriesConditionRule extends BaseElementsSelectConditionRule implements ElementConditionRuleInterface
{
/**
* @inheritdoc
*/
public function getLabel(): string
{
return Craft::t('app', 'Entries');
}

/**
* @inheritdoc
*/
protected function elementType(): string
{
return Entry::class;
}

/**
* @inheritdoc
*/
public function getExclusiveQueryParams(): array
{
return ['id'];
}

/**
* @inheritdoc
*/
public function modifyQuery(ElementQueryInterface $query): void
{
$elementIds = $this->getElementIds();

if ($this->operator === self::OPERATOR_NOT_IN) {
ArrayHelper::prependOrAppend($elementIds, 'not', true);
}
/** @var EntryQuery $query */
$query->id($elementIds);
}

/**
* @inheritdoc
*/
public function matchElement(ElementInterface $element): bool
{
/** @var Entry $element */
return $this->matchValue($element->id);
}
}
1 change: 1 addition & 0 deletions src/elements/conditions/entries/EntryCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ protected function selectableConditionRules(): array
return array_merge(parent::selectableConditionRules(), [
AuthorConditionRule::class,
AuthorGroupConditionRule::class,
EntriesConditionRule::class,
ExpiryDateConditionRule::class,
HasDescendantsRule::class,
LevelConditionRule::class,
Expand Down
2 changes: 2 additions & 0 deletions src/translations/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,7 @@
'This can be set to an environment variable with a valid language ID ({examples}).' => 'This can be set to an environment variable with a valid language ID ({examples}).',
'This can be set to an environment variable with a value of a [supported time zone]({url}).' => 'This can be set to an environment variable with a value of a [supported time zone]({url}).',
'This can be set to an environment variable, or a Twig template that outputs an ID.' => 'This can be set to an environment variable, or a Twig template that outputs an ID.',
'This can be set to an environment variable, or a Twig template that outputs a comma separated list of IDs.' => 'This can be set to an environment variable, or a Twig template that outputs a comma separated list of IDs.',
'This can be set to an environment variable, or begin with an alias.' => 'This can be set to an environment variable, or begin with an alias.',
'This can be set to an environment variable.' => 'This can be set to an environment variable.',
'This draft’s entry type is no longer available. You can still view it, but not apply it.' => 'This draft’s entry type is no longer available. You can still view it, but not apply it.',
Expand Down Expand Up @@ -2155,6 +2156,7 @@
'{type} Condition' => '{type} Condition',
'{type} Criteria' => '{type} Criteria',
'{type} ID' => '{type} ID',
'{type} IDs' => '{type} IDs',
'{type} Per Page' => '{type} Per Page',
'{type} Settings' => '{type} Settings',
'{type} Sources' => '{type} Sources',
Expand Down
Loading