diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 2074fae7928..bdbaa5595c8 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -7,6 +7,7 @@ - Element slideouts now show their sidebar content full-screen for elements without a field layout, rather than having an empty body. ([#13056](https://github.com/craftcms/cms/pull/13056), [#13053](https://github.com/craftcms/cms/issues/13053)) - Relational fields no longer track the previously-selected element(s) when something outside the field is clicked on. ([#13123](https://github.com/craftcms/cms/issues/13123)) - Element indexes now use field layouts’ overridden field labels, if all field layouts associated with an element source use the same label. ([#8903](https://github.com/craftcms/cms/discussions/8903)) +- Element indexes now track souces’ filters in the URL, so they can be sharable and persisted when navigating back to the index page via the browser history. ([#13499](https://github.com/craftcms/cms/pull/13499)) - Improved the styling and max height of Selectize inputs. ([#13065](https://github.com/craftcms/cms/discussions/13065), [#13176](https://github.com/craftcms/cms/pull/13176)) - Selectize inputs now support click-and-drag selection. ([#13273](https://github.com/craftcms/cms/discussions/13273)) - Selectize single-select inputs now automatically select the current value on focus. ([#13273](https://github.com/craftcms/cms/discussions/13273)) @@ -42,6 +43,7 @@ - When applying a draft, the canonical elements’ `getDirtyAttributes()` and `getDirtyFields()` methods now return the attribute names and field handles that were modified on the draft for save events. ([#12967](https://github.com/craftcms/cms/issues/12967)) - Admin tables can be configured to pass custom query params to the data endpoint. ([#13416](https://github.com/craftcms/cms/pull/13416)) - Admin tables can now be programatically reloaded. ([#13416](https://github.com/craftcms/cms/pull/13416)) +- Native element sources can now define a `defaultFilter` key, which defines the default filter condition that should be applied when the source is selected. ([#13499](https://github.com/craftcms/cms/pull/13499)) - Added `craft\addresses\SubdivisionRepository`. ([#13361](https://github.com/craftcms/cms/pull/13361)) - Added `craft\base\Element::thumbSvg()`. ([#13262](https://github.com/craftcms/cms/pull/13262)) - Added `craft\base\ElementInterface::getThumbHtml()`. diff --git a/src/base/ElementInterface.php b/src/base/ElementInterface.php index c0d805e5539..e7ae381b12b 100644 --- a/src/base/ElementInterface.php +++ b/src/base/ElementInterface.php @@ -279,6 +279,8 @@ public static function statuses(): array; * the attribute. (Optional) * - **`defaultSort`** – A string identifying the sort attribute that should be selected by default, or an array where * the first value identifies the sort attribute, and the second determines which direction to sort by. (Optional) + * - **`defaultFilter`** – An element condition instance or config, which should be used by default when the source + * is first selected. * - **`hasThumbs`** – A bool that defines whether this source supports Thumbs View. (Use your element’s * [[getThumbUrl()]] method to define your elements’ thumb URL.) (Optional) * - **`structureId`** – The ID of the Structure that contains the elements in this source. If set, Structure View diff --git a/src/controllers/ElementIndexesController.php b/src/controllers/ElementIndexesController.php index 6047cfc39f5..2ffe3a0c5ba 100644 --- a/src/controllers/ElementIndexesController.php +++ b/src/controllers/ElementIndexesController.php @@ -20,6 +20,7 @@ use craft\elements\db\ElementQueryInterface; use craft\elements\exporters\Raw; use craft\events\ElementActionEvent; +use craft\helpers\Component; use craft\helpers\Cp; use craft\helpers\ElementHelper; use craft\services\ElementSources; @@ -418,7 +419,24 @@ public function actionFilterHud(): Response /** @phpstan-var class-string|ElementInterface $elementType */ $elementType = $this->elementType(); $id = $this->request->getRequiredBodyParam('id'); - $condition = $elementType::createCondition(); + $conditionConfig = $this->request->getBodyParam('conditionConfig'); + $serialized = $this->request->getBodyParam('serialized'); + + $conditionsService = Craft::$app->getConditions(); + + if ($conditionConfig) { + $conditionConfig = Component::cleanseConfig($conditionConfig); + /** @var ElementConditionInterface $condition */ + $condition = $conditionsService->createCondition($conditionConfig); + } elseif ($serialized) { + parse_str($serialized, $conditionConfig); + /** @var ElementConditionInterface $condition */ + $condition = $conditionsService->createCondition($conditionConfig['condition']); + } else { + /** @var ElementConditionInterface $condition */ + $condition = $elementType::createCondition(); + } + $condition->mainTag = 'div'; $condition->id = $id; $condition->addRuleLabel = Craft::t('app', 'Add a filter'); @@ -429,7 +447,7 @@ public function actionFilterHud(): Response $condition->sourceKey = $this->sourceKey; } else { /** @var ElementConditionInterface $sourceCondition */ - $sourceCondition = Craft::$app->getConditions()->createCondition($this->source['condition']); + $sourceCondition = $conditionsService->createCondition($this->source['condition']); $condition->queryParams = []; foreach ($sourceCondition->getConditionRules() as $rule) { /** @var ElementConditionRuleInterface $rule */ @@ -595,11 +613,17 @@ protected function elementQuery(): ElementQueryInterface } // Override with the custom filters - $filterConditionStr = $this->request->getBodyParam('filters'); - if ($filterConditionStr) { - parse_str($filterConditionStr, $filterConditionConfig); + $filterConditionConfig = $this->request->getBodyParam('filterConfig'); + if (!$filterConditionConfig) { + $filterConditionStr = $this->request->getBodyParam('filters'); + if ($filterConditionStr) { + parse_str($filterConditionStr, $filterConditionConfig); + $filterConditionConfig = $filterConditionConfig['condition']; + } + } + if ($filterConditionConfig) { /** @var ElementConditionInterface $filterCondition */ - $filterCondition = $conditionsService->createCondition($filterConditionConfig['condition']); + $filterCondition = $conditionsService->createCondition(Component::cleanseConfig($filterConditionConfig)); $filterCondition->modifyQuery($query); } diff --git a/src/services/ElementSources.php b/src/services/ElementSources.php index 4f5606f74d5..94b4ce92e8b 100644 --- a/src/services/ElementSources.php +++ b/src/services/ElementSources.php @@ -8,6 +8,7 @@ namespace craft\services; use Craft; +use craft\base\conditions\ConditionInterface; use craft\base\ElementInterface; use craft\base\PreviewableFieldInterface; use craft\base\SortableFieldInterface; @@ -366,17 +367,25 @@ private function _nativeSources(string $elementType, string $context): array /** @phpstan-var class-string|ElementInterface $elementType */ $sources = $elementType::sources($context); $normalized = []; + foreach ($sources as $source) { - if (isset($source['type'])) { - $normalized[] = $source; - } elseif (array_key_exists('heading', $source)) { - $source['type'] = self::TYPE_HEADING; - $normalized[] = $source; - } elseif (isset($source['key'])) { - $source['type'] = self::TYPE_NATIVE; - $normalized[] = $source; + if (!isset($source['type'])) { + if (array_key_exists('heading', $source)) { + $source['type'] = self::TYPE_HEADING; + } elseif (isset($source['key'])) { + $source['type'] = self::TYPE_NATIVE; + } else { + continue; + } } + + if (isset($source['defaultFilter']) && $source['defaultFilter'] instanceof ConditionInterface) { + $source['defaultFilter'] = $source['defaultFilter']->getConfig(); + } + + $normalized[] = $source; } + return $normalized; } diff --git a/src/templates/_elements/sources.twig b/src/templates/_elements/sources.twig index 689fc61cd52..576eb3208b1 100644 --- a/src/templates/_elements/sources.twig +++ b/src/templates/_elements/sources.twig @@ -43,6 +43,7 @@ sites: (source.sites ?? false) ? source.sites|join(',') : false, 'override-status': (source.criteria.status ?? false) ? true : false, disabled: source.disabled ?? false, + 'default-filter': source.defaultFilter ?? false, }|merge(source.data ?? {}), html: _self.sourceInnerHtml(source) }) }} diff --git a/src/web/assets/cp/dist/cp.js b/src/web/assets/cp/dist/cp.js index f5df868ca36..752fb1b26d8 100644 --- a/src/web/assets/cp/dist/cp.js +++ b/src/web/assets/cp/dist/cp.js @@ -1,2 +1,2 @@ -(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 i=t.keyCode;i!==Garnish.SPACE_KEY&&i!==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 i=0;i .address-card");for(var s=0;s=this.settings.maxItems)){var e=$(t).appendTo(this.$tbody),i=e.find(".delete");this.settings.sortable&&this.sorter.addItems(e),this.$deleteBtns=this.$deleteBtns.add(i),this.addListener(i,"click","handleDeleteBtnClick"),this.totalItems++,this.updateUI()}},reorderItems:function(){var t=this;if(this.settings.sortable){for(var e=[],i=0;i=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 i=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=$('