From 30627244ad33b35caf146972318166161b4f54d4 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 8 Oct 2024 08:20:56 +0100 Subject: [PATCH 1/3] Add ability to have multiple element selection condition rules --- CHANGELOG-WIP.md | 3 + .../BaseElementsSelectConditionRule.php | 237 ++++++++++++++++++ .../entries/EntriesConditionRule.php | 68 +++++ .../conditions/entries/EntryCondition.php | 1 + src/translations/en/app.php | 2 + 5 files changed, 311 insertions(+) create mode 100644 src/base/conditions/BaseElementsSelectConditionRule.php create mode 100644 src/elements/conditions/entries/EntriesConditionRule.php diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index ac8184c5346..f415ad390e8 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -7,6 +7,7 @@ - Action button cells within editable tables are now center-aligned vertically. - 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)) +- Entry conditions can now have a “Entries” rule. ### Accessibility - Improved the control panel for screen readers. ([#15665](https://github.com/craftcms/cms/pull/15665)) @@ -20,7 +21,9 @@ - Added the `--except`, `--minor-only`, and `--patch-only` options to the `update` command. ([#15829](https://github.com/craftcms/cms/pull/15829)) ### 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)) diff --git a/src/base/conditions/BaseElementsSelectConditionRule.php b/src/base/conditions/BaseElementsSelectConditionRule.php new file mode 100644 index 00000000000..0d5e73fb9b5 --- /dev/null +++ b/src/base/conditions/BaseElementsSelectConditionRule.php @@ -0,0 +1,237 @@ + + * @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|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 $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, + }; + } +} diff --git a/src/elements/conditions/entries/EntriesConditionRule.php b/src/elements/conditions/entries/EntriesConditionRule.php new file mode 100644 index 00000000000..4d76e839049 --- /dev/null +++ b/src/elements/conditions/entries/EntriesConditionRule.php @@ -0,0 +1,68 @@ + + * @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); + } +} diff --git a/src/elements/conditions/entries/EntryCondition.php b/src/elements/conditions/entries/EntryCondition.php index de929730771..fcd5f386446 100644 --- a/src/elements/conditions/entries/EntryCondition.php +++ b/src/elements/conditions/entries/EntryCondition.php @@ -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, diff --git a/src/translations/en/app.php b/src/translations/en/app.php index 3b37f29cfff..bd1840e4433 100644 --- a/src/translations/en/app.php +++ b/src/translations/en/app.php @@ -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.', @@ -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', From 3d55d44019427435271709d1e5f231f4f0a4fa8a Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 8 Oct 2024 08:22:22 +0100 Subject: [PATCH 2/3] Changelog tweak --- CHANGELOG-WIP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 2ee3d98cd2c..fe72d2c8804 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -8,7 +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 Return when an inline-editable field is focused now submits the inline form. (Previously Ctrl/Command had to be pressed as well.) ([#15841](https://github.com/craftcms/cms/issues/15841)) -- Entry conditions can now have a “Entries” rule. +- Entry conditions can now have an “Entries” rule. ### Accessibility - Improved the control panel for screen readers. ([#15665](https://github.com/craftcms/cms/pull/15665)) From ab629155cfd3a42ba20e8d79bf256d938673cd82 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 8 Oct 2024 09:17:03 +0100 Subject: [PATCH 3/3] PHPstan fix --- src/base/conditions/BaseElementsSelectConditionRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/conditions/BaseElementsSelectConditionRule.php b/src/base/conditions/BaseElementsSelectConditionRule.php index 0d5e73fb9b5..65a844b3b08 100644 --- a/src/base/conditions/BaseElementsSelectConditionRule.php +++ b/src/base/conditions/BaseElementsSelectConditionRule.php @@ -110,7 +110,7 @@ public function getElementIds(bool $parse = true): array|string|null /** * @param array|string|null $elementIds - * @phpstan-param array|int|string|null $elementIds + * @phpstan-param array|string|null $elementIds */ public function setElementIds(array|string|null $elementIds): void {