diff --git a/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/FilterSettingTypeRendererCore.php b/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/FilterSettingTypeRendererCore.php index 79eff979c..2d34399cf 100644 --- a/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/FilterSettingTypeRendererCore.php +++ b/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/FilterSettingTypeRendererCore.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2024 The MetaModels team. + * (c) 2012-2026 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,7 +14,7 @@ * @author Christian Schiffler * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2012-2024 The MetaModels team. + * @copyright 2012-2026 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -37,14 +37,13 @@ class FilterSettingTypeRendererCore extends AbstractFilterSettingTypeRenderer #[\Override] protected function getTypes() { - return ['idlist', 'simplelookup', 'customsql', 'conditionand', 'conditionor']; + return ['idlist', 'simplelookup', 'customsql', 'conditionand', 'conditionor', 'conditiongate']; } /** * Retrieve the parameters for the label. * * @param EnvironmentInterface $environment The translator in use. - * * @param ModelInterface $model The model. * * @return array @@ -52,9 +51,10 @@ protected function getTypes() #[\Override] protected function getLabelParameters(EnvironmentInterface $environment, ModelInterface $model) { - if ($model->getProperty('type') == 'simplelookup') { + if ($model->getProperty('type') === 'simplelookup') { return $this->getLabelParametersWithAttributeAndUrlParam($environment, $model); } + return $this->getLabelParametersNormal($environment, $model); } } diff --git a/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/PasteButtonListener.php b/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/PasteButtonListener.php index 4aa148d2e..14b3665d3 100644 --- a/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/PasteButtonListener.php +++ b/src/CoreBundle/EventListener/DcGeneral/Table/FilterSetting/PasteButtonListener.php @@ -25,9 +25,11 @@ use ContaoCommunityAlliance\DcGeneral\Clipboard\Filter; use ContaoCommunityAlliance\DcGeneral\Clipboard\ItemInterface; use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\GetPasteButtonEvent; +use ContaoCommunityAlliance\DcGeneral\Controller\ModelCollector; use ContaoCommunityAlliance\DcGeneral\Data\ModelId; use ContaoCommunityAlliance\DcGeneral\Data\ModelInterface; use MetaModels\Filter\Setting\IFilterSettingFactory; +use MetaModels\Filter\Setting\IFilterSettingTypeFactory; /** * This class takes care of enabling and disabling of the paste button. @@ -41,6 +43,8 @@ class PasteButtonListener */ private IFilterSettingFactory $filterFactory; + private \SplObjectStorage $parents; + /** * Create a new instance. * @@ -49,6 +53,7 @@ class PasteButtonListener public function __construct(IFilterSettingFactory $filterFactory) { $this->filterFactory = $filterFactory; + $this->parents = new \SplObjectStorage(); } /** @@ -79,11 +84,45 @@ public function handle(GetPasteButtonEvent $event) return; } - $factory = $this->filterFactory->getTypeFactory($model->getProperty('type')); + $factory = $this->getFactoryFor($model); + if (null === $factory) { + // Unknown type, disallow paste. + $event->setPasteIntoDisabled(true); + $event->setPasteAfterDisabled(true); + return; + } // If setting does not support children, omit them. - if ($model->getId() && !($factory && $factory->isNestedType())) { + if ($model->getId() && !($factory->isNestedType())) { $event->setPasteIntoDisabled(true); } + + $collector = new ModelCollector($event->getEnvironment()); + if ($factory->isNestedType() && (null !== ($maxChildren = $factory->getMaxChildren()))) { + if ($maxChildren < count($collector->collectDirectChildrenOf($model))) { + $event->setPasteIntoDisabled(true); + } + } + if (!$this->parents->contains($model)) { + $this->parents[$model] = $collector->searchParentOf($model); + } + $parent = $this->parents[$model]; + if (!$parent) { + return; + } + $parentFactory = $this->getFactoryFor($parent); + if (!$parentFactory?->isNestedType() || (null === ($maxChildren = $parentFactory?->getMaxChildren()))) { + return; + } + $siblings = $collector->collectSiblingsOf($model, $parent?->getId()); + // FIXME: Except, if we are already contained and just get moved within parent :( + if ($maxChildren <= $siblings->length()) { + $event->setPasteAfterDisabled(true); + } + } + + private function getFactoryFor(ModelInterface $model): ?IFilterSettingTypeFactory + { + return $this->filterFactory->getTypeFactory($model->getProperty('type')); } } diff --git a/src/CoreBundle/Resources/config/filter-settings.yml b/src/CoreBundle/Resources/config/filter-settings.yml index 7c2e27824..d244ab9f4 100644 --- a/src/CoreBundle/Resources/config/filter-settings.yml +++ b/src/CoreBundle/Resources/config/filter-settings.yml @@ -54,3 +54,11 @@ services: - '@translator' tags: - { name: metamodels.filter_factory } + + MetaModels\Filter\Setting\ConditionGateFilterSettingTypeFactory: + arguments: + - '@metamodels.expression-language' + - '@request_stack' + - '@translator' + tags: + - { name: metamodels.filter_factory } diff --git a/src/CoreBundle/Resources/config/services.yml b/src/CoreBundle/Resources/config/services.yml index 4152cf2c8..fbc728799 100644 --- a/src/CoreBundle/Resources/config/services.yml +++ b/src/CoreBundle/Resources/config/services.yml @@ -356,3 +356,6 @@ services: decorates: 'cca.backend-help-provider' arguments: $previous: '@.inner' + + metamodels.expression-language: + class: Symfony\Component\ExpressionLanguage\ExpressionLanguage diff --git a/src/CoreBundle/Resources/contao/dca/tl_metamodel_filtersetting.php b/src/CoreBundle/Resources/contao/dca/tl_metamodel_filtersetting.php index 242c66f6c..8899228d7 100644 --- a/src/CoreBundle/Resources/contao/dca/tl_metamodel_filtersetting.php +++ b/src/CoreBundle/Resources/contao/dca/tl_metamodel_filtersetting.php @@ -94,7 +94,14 @@ 'remote' => 'id', 'operation' => '=', ], - ] + ], + 'inverse' => [ + [ + 'local' => 'pid', + 'remote' => 'id', + 'operation' => '=', + ], + ], ] ], 'rootEntries' => [ @@ -223,6 +230,14 @@ 'stop_after_match' ] ], + 'conditiongate extends default' => [ + 'config' => [ + 'gate_expression' + ], + '+fefilter' => [ + 'onlypossible', + ], + ], 'idlist extends default' => [ '+config' => [ 'items' @@ -450,6 +465,19 @@ ], 'sql' => "char(1) NOT NULL default ''" ], + 'gate_expression' => [ + 'label' => 'gate_expression.label', + 'description' => 'gate_expression.description', + 'exclude' => true, + 'inputType' => 'text', + 'eval' => [ + 'alwaysSave' => true, + 'decodeEntities' => true, + 'mandatory' => true, + 'tl_class' => 'clr', + ], + 'sql' => "text NOT NULL default ''" + ], 'label' => [ 'label' => 'label.label', 'description' => 'label.description', diff --git a/src/CoreBundle/Resources/translations/tl_metamodel_filtersetting.en.xlf b/src/CoreBundle/Resources/translations/tl_metamodel_filtersetting.en.xlf index 607b30cbf..f24a8dc06 100644 --- a/src/CoreBundle/Resources/translations/tl_metamodel_filtersetting.en.xlf +++ b/src/CoreBundle/Resources/translations/tl_metamodel_filtersetting.en.xlf @@ -298,6 +298,12 @@ AND condition + + Gate condition + + + The condition "%name%" can only have "%max%" children max. + <em>[%colName%, "%name%"]</em> @@ -322,6 +328,9 @@ %1$s <strong>%2$s</strong> %4$s + + %1$s <strong>%2$s</strong> %4$s + Items diff --git a/src/Filter/Rules/Condition/ConditionGate.php b/src/Filter/Rules/Condition/ConditionGate.php new file mode 100644 index 000000000..97542287b --- /dev/null +++ b/src/Filter/Rules/Condition/ConditionGate.php @@ -0,0 +1,37 @@ +|null */ + #[\Override] + public function getMatchingIds(): ?array + { + if ((bool) $this->expressionLanguage->evaluate($this->expression, $this->parameters)) { + return $this->ifTrue?->getMatchingIds(); + } + if (null !== $this->ifFalse) { + return $this->ifFalse->getMatchingIds(); + } + return []; + } +} diff --git a/src/Filter/Setting/Condition/ConditionGate.php b/src/Filter/Setting/Condition/ConditionGate.php new file mode 100644 index 000000000..99ac1bc6e --- /dev/null +++ b/src/Filter/Setting/Condition/ConditionGate.php @@ -0,0 +1,222 @@ + + * @author David Maack + * @author Sven Baumann + * @copyright 2012-2019 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +namespace MetaModels\Filter\Setting\Condition; + +use Contao\Message; +use MetaModels\Filter\IFilter; +use MetaModels\Filter\Rules\Condition\ConditionGate as FilterRuleGate; +use MetaModels\Filter\Setting\ISimple; +use MetaModels\Filter\Setting\IWithChildren; +use MetaModels\FrontendIntegration\FrontendFilterOptions; +use MetaModels\IItem; +use MetaModels\IMetaModel; +use MetaModels\Render\Setting\ICollection as IRenderSettings; +use Override; +use RuntimeException; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Contracts\Translation\TranslatorInterface; + +use function array_merge; +use function count; + +/** + * This filter condition generates a filter rule, that represents a simple "if this then that else that". + */ +final class ConditionGate implements IWithChildren +{ + /** @var list */ + private array $children = []; + + public function __construct( + private readonly array $data, + private readonly ExpressionLanguage $expressionLanguage, + private readonly RequestStack $requestStack, + private readonly IMetaModel $metaModel, + private readonly TranslatorInterface $translator, + ) { + } + + #[Override] + public function prepareRules(IFilter $objFilter, $arrFilterUrl): void + { + $ifTrue = null; + if ($this->children[0] ?? null) { + $ifTrue = $this->metaModel->getEmptyFilter(); + $this->children[0]->prepareRules($ifTrue, $arrFilterUrl); + } + $ifFalse = null; + if ($this->children[1] ?? null) { + $ifFalse = $this->metaModel->getEmptyFilter(); + $this->children[1]->prepareRules($ifFalse, $arrFilterUrl); + } + + $filterRule = new FilterRuleGate( + $this->getExpression(), + $this->getExpressionParameters($arrFilterUrl), + $this->expressionLanguage, + $ifTrue, + $ifFalse, + ); + + $objFilter->addFilterRule($filterRule); + } + + #[Override] + public function addChild(ISimple $objFilterSetting): void + { + if (count($this->children) >= 2) { + // FIXME: call getTypeName() for name. + Message::addInfo( + $this->translator->trans( + 'error.condition_max_children', + ['%name%' => 'conditiongate', '%max%' => '2'], + 'tl_metamodel_filtersetting' + ) + ); + return; + //throw new RuntimeException('A condition gate can only have two children max.'); + } + $this->children[] = $objFilterSetting; + } + + #[Override] + public function get($strKey): mixed + { + return $this->data[$strKey] ?? null; + } + + #[Override] + public function generateFilterUrlFrom(IItem $objItem, IRenderSettings $objRenderSetting): array + { + $result = []; + foreach ($this->children as $child) { + $result[] = $child->generateFilterUrlFrom($objItem, $objRenderSetting); + } + + return array_merge(...$result); + } + + #[Override] + public function getParameters(): array + { + $parameters = []; + foreach ($this->children as $child) { + $parameters[] = $child->getParameters(); + } + + return array_merge(...$parameters); + } + + #[Override] + public function getParameterDCA(): array + { + $parameters = []; + foreach ($this->children as $child) { + $parameters[] = $child->getParameterDCA(); + } + + return array_merge(...$parameters); + } + + #[Override] + public function getParameterFilterNames(): array + { + $parameters = []; + foreach ($this->children as $objSetting) { + $parameters[] = $objSetting->getParameterFilterNames(); + } + + return array_merge(...$parameters); + } + + /** @SuppressWarnings(PHPMD.LongVariable) */ + #[Override] + public function getParameterFilterWidgets( + $arrIds, + $arrFilterUrl, + $arrJumpTo, + FrontendFilterOptions $objFrontendFilterOptions + ): array { + if (null !== ($child = $this->children[0])) { + return $child->getParameterFilterWidgets( + $this->isConditionFulfilled($arrFilterUrl) && ((bool) $this->get('onlypossible')) ? $arrIds : null, + $arrFilterUrl, + $arrJumpTo, + $objFrontendFilterOptions + ); + } + // TODO: besser weiter hirnen... + // $child = $this->getFilterForConditionState($arrFilterUrl); + // if ($child) { + // return $child->getParameterFilterWidgets( + // ((bool) $this->get('onlypossible')) ? $arrIds : null, + // $arrFilterUrl, + // $arrJumpTo, + // $objFrontendFilterOptions + // ); + // } + + return []; + } + + #[Override] + public function getReferencedAttributes(): array + { + $attributes = []; + foreach ($this->children as $child) { + $attributes[] = $child->getReferencedAttributes(); + } + + return array_merge(...$attributes); + } + + private function getFilterForConditionState(array $filterUrl): ?ISimple + { + if ($this->isConditionFulfilled($filterUrl)) { + return $this->children[0] ?? null; + } + + return $this->children[1] ?? null; + } + + private function isConditionFulfilled(array $filterUrl): bool + { + return (bool) $this->expressionLanguage->evaluate( + $this->getExpression(), + $this->getExpressionParameters($filterUrl) + ); + } + + private function getExpression(): string + { + return (string) $this->data['gate_expression']; + } + + private function getExpressionParameters(array $arrFilterUrl): array + { + return [ + 'filterUrl' => $arrFilterUrl, + 'request' => $this->requestStack->getCurrentRequest(), + ]; + } +} diff --git a/src/Filter/Setting/ConditionAndFilterSettingTypeFactory.php b/src/Filter/Setting/ConditionAndFilterSettingTypeFactory.php index cb0c1379a..e4a788036 100644 --- a/src/Filter/Setting/ConditionAndFilterSettingTypeFactory.php +++ b/src/Filter/Setting/ConditionAndFilterSettingTypeFactory.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2024 The MetaModels team. + * (c) 2012-2026 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,7 +14,7 @@ * @author Christian Schiffler * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2012-2024 The MetaModels team. + * @copyright 2012-2026 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -43,7 +43,7 @@ public function __construct( $this ->setTypeName('conditionand') - ->setTypeIcon('/bundles/metamodelscore/images/icons/filter_and.png') + ->setTypeIcon('bundles/metamodelscore/images/icons/filter_and.png') ->setTypeClass(ConditionAnd::class) ->allowAttributeTypes(); } diff --git a/src/Filter/Setting/ConditionGateFilterSettingTypeFactory.php b/src/Filter/Setting/ConditionGateFilterSettingTypeFactory.php new file mode 100644 index 000000000..7c75a510d --- /dev/null +++ b/src/Filter/Setting/ConditionGateFilterSettingTypeFactory.php @@ -0,0 +1,65 @@ +expressionLanguage, + $this->requestStack, + $filterSettings->getMetaModel(), + $this->translator, + ); + } +}