From 886fed224bf54030ddd1ba7866f2fa1833b83160 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Thu, 25 Apr 2024 16:13:33 -0700 Subject: [PATCH] Pass the field layout config to element conditions Fixes #14787 --- CHANGELOG-WIP.md | 4 + src/base/FieldLayoutComponent.php | 13 ++- src/base/conditions/BaseCondition.php | 12 ++- src/base/conditions/ConditionInterface.php | 8 ++ src/controllers/FieldsController.php | 102 +++++++++++++----- src/elements/conditions/ElementCondition.php | 44 +++++++- .../conditions/ElementConditionInterface.php | 8 ++ src/helpers/Cp.php | 37 +------ src/web/assets/cp/dist/cp.js | 2 +- src/web/assets/cp/dist/cp.js.map | 2 +- .../assets/cp/src/js/FieldLayoutDesigner.js | 89 +++++++-------- 11 files changed, 203 insertions(+), 118 deletions(-) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 61f41113fc9..f9907e48606 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -7,13 +7,16 @@ - Element tooltips now appear after a half-second delay. ([#14836](https://github.com/craftcms/cms/issues/14836)) ### Administration +- Element conditions within field layout designers’ component settings now only list custom fields present in the current field layout. ([#14787](https://github.com/craftcms/cms/issues/14787)) - Added the `asyncCsrfInputs` config setting. ([#14625](https://github.com/craftcms/cms/pull/14625)) - Added the `safeMode` config setting. ([#14734](https://github.com/craftcms/cms/pull/14734)) - `resave` commands now support an `--if-invalid` option. ([#14731](https://github.com/craftcms/cms/issues/14731)) ### Extensibility +- Added `craft\conditions\ConditionInterface::getBuilderConfig()`. - Added `craft\controllers\EditUserTrait`. ([#14789](https://github.com/craftcms/cms/pull/14789)) - Added `craft\controllers\UsersController::EVENT_DEFINE_EDIT_SCREENS`. ([#14789](https://github.com/craftcms/cms/pull/14789)) +- Added `craft\elements\conditions\ElementConditionInterface::setFieldLayouts()`. - Added `craft\events\DefineEditUserScreensEvent`. ([#14789](https://github.com/craftcms/cms/pull/14789)) - Added `craft\services\ProjectConfig::getAppliedChanges()`. ([#14851](https://github.com/craftcms/cms/discussions/14851)) - Added `craft\web\Request::getBearerToken()`. ([#14784](https://github.com/craftcms/cms/pull/14784)) @@ -27,3 +30,4 @@ ### System - Batched queue jobs now set their progress based on the total progress across all batches, rather than just the current batch. ([#14817](https://github.com/craftcms/cms/pull/14817)) - Fixed a bug where ordering by a custom field would only partially work, if the custom field was included in multiple field layouts for the resulting elements. ([#14821](https://github.com/craftcms/cms/issues/14821)) +- Fixed a bug where element conditions within field layout designers’ component settings weren’t listing custom fields which were just added to the layout. ([#14787](https://github.com/craftcms/cms/issues/14787)) diff --git a/src/base/FieldLayoutComponent.php b/src/base/FieldLayoutComponent.php index 58526f45c7a..aad243a3455 100644 --- a/src/base/FieldLayoutComponent.php +++ b/src/base/FieldLayoutComponent.php @@ -168,6 +168,13 @@ public function setUserCondition(mixed $userCondition): void public function getElementCondition(): ?ElementConditionInterface { if (isset($this->_elementCondition) && !$this->_elementCondition instanceof ElementConditionInterface) { + if (is_string($this->_elementCondition)) { + $this->_elementCondition = ['class' => $this->_elementCondition]; + } + $this->_elementCondition = array_merge( + ['fieldLayouts' => [$this->getLayout()]], + $this->_elementCondition, + ); $this->_elementCondition = $this->_normalizeCondition($this->_elementCondition); } @@ -266,7 +273,11 @@ public function getSettingsHtml(): string $elementType = $this->elementType ?? $this->getLayout()->type; if ($elementType && is_subclass_of($elementType, ElementInterface::class)) { - $elementCondition = $this->getElementCondition() ?? self::defaultElementCondition($elementType); + $elementCondition = $this->getElementCondition(); + if (!$elementCondition) { + $elementCondition = clone self::defaultElementCondition($elementType); + $elementCondition->setFieldLayouts([$this->getLayout()]); + } $elementCondition->mainTag = 'div'; $elementCondition->id = 'element-condition'; $elementCondition->name = 'elementCondition'; diff --git a/src/base/conditions/BaseCondition.php b/src/base/conditions/BaseCondition.php index bc7ce575065..d8c46866576 100644 --- a/src/base/conditions/BaseCondition.php +++ b/src/base/conditions/BaseCondition.php @@ -290,7 +290,7 @@ public function getBuilderInnerHtml(bool $autofocusAddButton = false): string ]); $html .= Html::hiddenInput('class', get_class($this)); - $html .= Html::hiddenInput('config', Json::encode($this->config())); + $html .= Html::hiddenInput('config', Json::encode($this->getBuilderConfig())); foreach ($this->getConditionRules() as $rule) { try { @@ -587,6 +587,14 @@ protected function defineRules(): array ]; } + /** + * @inheritdoc + */ + public function getBuilderConfig(): array + { + return $this->config(); + } + /** * @inheritdoc */ @@ -610,7 +618,7 @@ public function getConfig(): array } /** - * Returns the condition’s portable config. + * Returns the base config that should be maintained by the builder and included in the condition’s portable config. * * @return array */ diff --git a/src/base/conditions/ConditionInterface.php b/src/base/conditions/ConditionInterface.php index d18803c3040..ab1c99e6981 100644 --- a/src/base/conditions/ConditionInterface.php +++ b/src/base/conditions/ConditionInterface.php @@ -31,6 +31,14 @@ public function getBuilderHtml(): string; */ public function getBuilderInnerHtml(bool $autofocusAddButton = false): string; + /** + * Returns configuration that should be maintained for the builder. + * + * @return array + * @since 5.1.0 + */ + public function getBuilderConfig(): array; + /** * Returns the condition’s portable config. * diff --git a/src/controllers/FieldsController.php b/src/controllers/FieldsController.php index 41fda6ed651..ea6bc974eaa 100644 --- a/src/controllers/FieldsController.php +++ b/src/controllers/FieldsController.php @@ -14,6 +14,8 @@ use craft\base\ElementInterface; use craft\base\Field; use craft\base\FieldInterface; +use craft\base\FieldLayoutComponent; +use craft\base\FieldLayoutElement; use craft\base\FieldLayoutProviderInterface; use craft\base\Iconic; use craft\fieldlayoutelements\CustomField; @@ -420,40 +422,40 @@ public function actionDeleteField(): ?Response // ------------------------------------------------------------------------- /** - * Applies a field layout tab’s settings. + * Renders a field layout component’s settings. * - * @return Response - * @throws BadRequestHttpException - * @since 4.0.0 + * @since 5.1.0 */ - public function actionApplyLayoutTabSettings(): Response + public function actionRenderLayoutComponentSettings(): Response { - $tab = new FieldLayoutTab($this->_fldComponentConfig()); + $element = $this->_fldComponent(); + $namespace = StringHelper::randomString(10); + $view = Craft::$app->getView(); + $html = $view->namespaceInputs(fn() => $element->getSettingsHtml(), $namespace); return $this->asJson([ - 'config' => $tab->toArray(), - 'hasConditions' => $tab->hasConditions(), + 'settingsHtml' => $html, + 'namespace' => $namespace, + 'headHtml' => $view->getHeadHtml(), + 'bodyHtml' => $view->getBodyHtml(), ]); } /** - * Renders a field layout element’s settings. + * Applies a field layout tab’s settings. * - * @since 5.0.0 + * @return Response + * @throws BadRequestHttpException + * @since 4.0.0 */ - public function actionRenderLayoutElementSettings(): Response + public function actionApplyLayoutTabSettings(): Response { - $element = Craft::$app->getFields()->createLayoutElement($this->_fldComponentConfig()); - $namespace = StringHelper::randomString(10); - - $view = Craft::$app->getView(); - $html = $view->namespaceInputs(fn() => $element->getSettingsHtml(), $namespace); + /** @var FieldLayoutTab $tab */ + $tab = $this->_fldComponent(); return $this->asJson([ - 'settingsHtml' => $html, - 'namespace' => $namespace, - 'headHtml' => $view->getHeadHtml(), - 'bodyHtml' => $view->getBodyHtml(), + 'config' => $tab->toArray(), + 'hasConditions' => $tab->hasConditions(), ]); } @@ -466,7 +468,8 @@ public function actionRenderLayoutElementSettings(): Response */ public function actionApplyLayoutElementSettings(): Response { - $element = Craft::$app->getFields()->createLayoutElement($this->_fldComponentConfig()); + /** @var FieldLayoutElement $element */ + $element = $this->_fldComponent(); if ($element instanceof CustomField) { $field = $element->getField(); @@ -516,22 +519,65 @@ public function actionTableData(): Response } /** - * Returns the posted settings. + * Returns the field layout component being edited, populated with the posted config/settings. * - * @return array + * @return FieldLayoutComponent */ - private function _fldComponentConfig(): array + private function _fldComponent(): FieldLayoutComponent { - $config = $this->request->getRequiredBodyParam('config'); - $config['elementType'] = $this->request->getRequiredBodyParam('elementType'); + $uid = $this->request->getRequiredBodyParam('uid'); + $elementType = $this->request->getRequiredBodyParam('elementType'); + $layoutConfig = $this->request->getRequiredBodyParam('layoutConfig'); + + if (!isset($layoutConfig['tabs'])) { + throw new BadRequestHttpException('Layout config doesn’t have any tabs.'); + } + + $layoutConfig['type'] = $elementType; + + $componentConfig = $this->request->getBodyParam('config') ?? []; + $componentConfig['elementType'] = $elementType; $settingsStr = $this->request->getBodyParam('settings'); if ($settingsStr !== null) { parse_str($settingsStr, $settings); $settingsNamespace = $this->request->getRequiredBodyParam('settingsNamespace'); - $config = array_merge($config, ArrayHelper::getValue($settings, $settingsNamespace, [])); + $componentConfig = array_merge($componentConfig, ArrayHelper::getValue($settings, $settingsNamespace, [])); } - return $config; + $isTab = false; + + foreach ($layoutConfig['tabs'] as &$tabConfig) { + if (isset($tabConfig['uid']) && $tabConfig['uid'] === $uid) { + $isTab = true; + $tabConfig = array_merge($tabConfig, $componentConfig); + break; + } + + foreach ($tabConfig['elements'] as &$elementConfig) { + if (isset($elementConfig['uid']) && $elementConfig['uid'] === $uid) { + $elementConfig = array_merge($elementConfig, $componentConfig); + break 2; + } + } + } + + $layout = Craft::$app->getFields()->createLayout($layoutConfig); + + if ($isTab) { + foreach ($layout->getTabs() as $tab) { + if ($tab->uid === $uid) { + return $tab; + } + } + + throw new BadRequestHttpException("Invalid layout tab UUID: $uid"); + } + + $element = $layout->getElementByUid($uid); + if (!$element) { + throw new BadRequestHttpException("Invalid layout element UUID: $uid"); + } + return $element; } } diff --git a/src/elements/conditions/ElementCondition.php b/src/elements/conditions/ElementCondition.php index 0a4e7352e09..01837fd8796 100644 --- a/src/elements/conditions/ElementCondition.php +++ b/src/elements/conditions/ElementCondition.php @@ -9,6 +9,7 @@ use craft\elements\db\ElementQueryInterface; use craft\errors\InvalidTypeException; use craft\fields\conditions\FieldConditionRuleInterface; +use craft\models\FieldLayout; use yii\base\InvalidConfigException; /** @@ -54,6 +55,13 @@ class ElementCondition extends BaseCondition implements ElementConditionInterfac */ public ?ElementInterface $referenceElement = null; + /** + * @var FieldLayout[] + * @see getFieldLayouts() + * @see setFieldLayouts() + */ + private array $_fieldLayouts; + /** * Constructor. * @@ -77,8 +85,15 @@ public function __construct(?string $elementType = null, array $config = []) parent::__construct($config); } + /** + * @inheritdoc + */ public function getFieldLayouts(): array { + if (isset($this->_fieldLayouts)) { + return $this->_fieldLayouts; + } + if ($this->elementType === null) { return []; } @@ -91,6 +106,21 @@ public function getFieldLayouts(): array return Craft::$app->getFields()->getLayoutsByType($this->elementType); } + /** + * @inheritdoc + */ + public function setFieldLayouts(array $fieldLayouts): void + { + $fieldsService = Craft::$app->getFields(); + $this->_fieldLayouts = array_map(function(FieldLayout|array $fieldLayout) use ($fieldsService) { + if (is_array($fieldLayout)) { + $fieldLayout['type'] = $this->elementType; + return $fieldsService->createLayout($fieldLayout); + } + return $fieldLayout; + }, $fieldLayouts); + } + /** * @inheritdoc */ @@ -198,10 +228,22 @@ protected function selectableConditionRules(): array protected function defineRules(): array { $rules = parent::defineRules(); - $rules[] = [['elementType', 'fieldContext'], 'safe']; + $rules[] = [['elementType', 'fieldLayouts', 'fieldContext'], 'safe']; return $rules; } + /** + * @inheritdoc + */ + public function getBuilderConfig(): array + { + $config = parent::getBuilderConfig(); + if (isset($this->_fieldLayouts)) { + $config['fieldLayouts'] = array_map(fn(FieldLayout $layout) => $layout->getConfig(), $this->_fieldLayouts); + } + return $config; + } + /** * @inheritdoc */ diff --git a/src/elements/conditions/ElementConditionInterface.php b/src/elements/conditions/ElementConditionInterface.php index a34649cb03e..e674b93eec1 100644 --- a/src/elements/conditions/ElementConditionInterface.php +++ b/src/elements/conditions/ElementConditionInterface.php @@ -32,6 +32,14 @@ interface ElementConditionInterface extends ConditionInterface */ public function getFieldLayouts(): array; + /** + * Sets the possible field layouts that the condition could be working with. + * + * @param array $fieldLayouts + * @since 5.1.0 + */ + public function setFieldLayouts(array $fieldLayouts): void; + /** * Modifies a given query based on the configured condition rules. * diff --git a/src/helpers/Cp.php b/src/helpers/Cp.php index 657854cd7f3..d0aaa0223d5 100644 --- a/src/helpers/Cp.php +++ b/src/helpers/Cp.php @@ -2185,21 +2185,10 @@ public static function fieldLayoutDesignerHtml(FieldLayout $fieldLayout, array $ $fieldLayoutConfig['id'] = $fieldLayout->id; } - $newTabSettingsData = self::_fldTabSettingsData(new FieldLayoutTab([ - 'uid' => 'TAB_UID', - 'name' => 'TAB_NAME', - 'layout' => $fieldLayout, - ])); - return Html::beginTag('div', [ 'id' => $config['id'], 'class' => 'layoutdesigner', - 'data' => [ - 'new-tab-settings-namespace' => $newTabSettingsData['settings-namespace'], - 'new-tab-settings-html' => $newTabSettingsData['settings-html'], - 'new-tab-settings-js' => $newTabSettingsData['settings-js'], - ], ]) . Html::hiddenInput('fieldLayout', Json::encode($fieldLayoutConfig), [ 'data' => ['config-input' => true], @@ -2284,9 +2273,9 @@ private static function _fldTabHtml(FieldLayoutTab $tab, bool $customizable): st return Html::beginTag('div', [ 'class' => 'fld-tab', - 'data' => array_merge([ + 'data' => [ 'uid' => $tab->uid, - ], self::_fldTabSettingsData($tab)), + ], ]) . Html::beginTag('div', ['class' => 'tabs']) . Html::beginTag('div', [ @@ -2322,28 +2311,6 @@ private static function _fldTabHtml(FieldLayoutTab $tab, bool $customizable): st Html::endTag('div'); // .fld-tab } - /** - * @param FieldLayoutTab $tab - * @return array - */ - private static function _fldTabSettingsData(FieldLayoutTab $tab): array - { - $view = Craft::$app->getView(); - $oldNamespace = $view->getNamespace(); - $namespace = $view->namespaceInputName("tab-$tab->uid"); - $view->setNamespace($namespace); - $view->startJsBuffer(); - $settingsHtml = $view->namespaceInputs($tab->getSettingsHtml()); - $settingsJs = $view->clearJsBuffer(false); - $view->setNamespace($oldNamespace); - - return [ - 'settings-namespace' => $namespace, - 'settings-html' => $settingsHtml, - 'settings-js' => $settingsJs, - ]; - } - /** * Renders a field layout element’s selector HTML. * diff --git a/src/web/assets/cp/dist/cp.js b/src/web/assets/cp/dist/cp.js index 759291e9d2b..68a77d92409 100644 --- a/src/web/assets/cp/dist/cp.js +++ b/src/web/assets/cp/dist/cp.js @@ -1,3 +1,3 @@ /*! For license information please see cp.js.LICENSE.txt */ -(function(){var __webpack_modules__={463:function(){Craft.Accordion=Garnish.Base.extend({$trigger:null,targetSelector:null,_$target:null,init:function(t){var e=this;this.$trigger=$(t),this.$trigger.data("accordion")&&(console.warn("Double-instantiating an accordion trigger on an element"),this.$trigger.data("accordion").destroy()),this.$trigger.data("accordion",this),this.targetSelector=this.$trigger.attr("aria-controls")?"#".concat(this.$trigger.attr("aria-controls")):null,this.targetSelector&&(this._$target=$(this.targetSelector)),this.addListener(this.$trigger,"click","onTriggerClick"),this.addListener(this.$trigger,"keypress",(function(t){var n=t.keyCode;n!==Garnish.SPACE_KEY&&n!==Garnish.RETURN_KEY||(t.preventDefault(),e.onTriggerClick())}))},onTriggerClick:function(){"true"===this.$trigger.attr("aria-expanded")?this.hideTarget(this._$target):this.showTarget(this._$target)},showTarget:function(t){var e=this;if(t&&t.length){this.showTarget._currentHeight=t.height(),t.removeClass("hidden"),this.$trigger.removeClass("collapsed").addClass("expanded").attr("aria-expanded","true");for(var n=0;n=this.settings.maxItems)){var e=$(t).appendTo(this.$tbody),n=e.find(".delete");this.settings.sortable&&this.sorter.addItems(e),this.$deleteBtns=this.$deleteBtns.add(n),this.addListener(n,"click","handleDeleteBtnClick"),this.totalItems++,this.updateUI()}},reorderItems:function(){var t=this;if(this.settings.sortable){for(var e=[],n=0;n=this.settings.maxItems?$(this.settings.newItemBtnSelector).addClass("hidden"):$(this.settings.newItemBtnSelector).removeClass("hidden"))}},{defaults:{tableSelector:null,noItemsSelector:null,newItemBtnSelector:null,idAttribute:"data-id",nameAttribute:"data-name",sortable:!1,allowDeleteAll:!0,minItems:0,maxItems:null,reorderAction:null,deleteAction:null,reorderSuccessMessage:Craft.t("app","New order saved."),reorderFailMessage:Craft.t("app","Couldn’t save new order."),confirmDeleteMessage:Craft.t("app","Are you sure you want to delete “{name}”?"),deleteSuccessMessage:Craft.t("app","“{name}” deleted."),deleteFailMessage:Craft.t("app","Couldn’t delete “{name}”."),onReorderItems:$.noop,onDeleteItem:$.noop}})},6872:function(){Craft.AssetImageEditor=Garnish.Modal.extend({$body:null,$footer:null,$imageTools:null,$buttons:null,$cancelBtn:null,$replaceBtn:null,$saveBtn:null,$focalPointBtn:null,$editorContainer:null,$straighten:null,$croppingCanvas:null,$spinner:null,$constraintContainer:null,$constraintRadioInputs:null,$customConstraints:null,canvas:null,image:null,viewport:null,focalPoint:null,grid:null,croppingCanvas:null,clipper:null,croppingRectangle:null,cropperHandles:null,cropperGrid:null,croppingShade:null,imageStraightenAngle:0,viewportRotation:0,originalWidth:0,originalHeight:0,imageVerticeCoords:null,zoomRatio:1,animationInProgress:!1,currentView:"",assetId:null,cacheBust:null,draggingCropper:!1,scalingCropper:!1,draggingFocal:!1,previousMouseX:0,previousMouseY:0,shiftKeyHeld:!1,editorHeight:0,editorWidth:0,cropperState:!1,scaleFactor:1,flipData:{},focalPointState:!1,maxImageSize:null,lastLoadedDimensions:null,imageIsLoading:!1,mouseMoveEvent:null,croppingConstraint:!1,constraintOrientation:"landscape",showingCustomConstraint:!1,saving:!1,renderImage:null,renderCropper:null,_queue:null,init:function(t,e){var n=this;this._queue=new Craft.Queue,this.cacheBust=Date.now(),this.setSettings(e,Craft.AssetImageEditor.defaults),null===this.settings.allowDegreeFractions&&(this.settings.allowDegreeFractions=Craft.isImagick),Garnish.prefersReducedMotion()&&(this.settings.animationDuration=1),this.assetId=t,this.flipData={x:0,y:0},this.$container=$('').appendTo(Garnish.$bod),this.$body=$('
').appendTo(this.$container),this.$footer=$('