diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 28ad75db68e..fe79ff519ad 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -5,6 +5,7 @@ ### Development - Added support for referencing environment variables anywhere within settings that support them (e.g. `foo/$ENV_NAME/bar` or `foo-${ENV_NAME}-bar`). ([#17949](https://github.com/craftcms/cms/pull/17949)) +- Added the `uuid()` Twig function. ### Extensibility - Added `craft\web\GqlResponseFormatter`. @@ -20,3 +21,4 @@ - GraphQL API responses now set cache headers based on whether a mutation was performed, regardless of the request type. - Global set queries no longer register cache tags. - Updated Twig to 3.19. ([#17603](https://github.com/craftcms/cms/discussions/17603)) +- Fixed a bug where Table fields with the “Static Rows” setting enabled would lose track of which values belonged to which row headings, if the “Default Values” table was reordered. ([#17090](https://github.com/craftcms/cms/issues/17090)) diff --git a/src/fields/Table.php b/src/fields/Table.php index 50db9215895..e1f365912c7 100644 --- a/src/fields/Table.php +++ b/src/fields/Table.php @@ -14,6 +14,7 @@ use craft\gql\GqlEntityRegistry; use craft\gql\types\generators\TableRowType; use craft\gql\types\TableRow; +use craft\helpers\ArrayHelper; use craft\helpers\Cp; use craft\helpers\DateTimeHelper; use craft\helpers\Db; @@ -338,7 +339,8 @@ public function getSettingsHtml(): ?string Json::encode($this->defaults ?? []) . ', ' . Json::encode($columnSettings) . ', ' . Json::encode($dropdownSettingsHtml) . ', ' . - Json::encode($dropdownSettingsCols) . + Json::encode($dropdownSettingsCols) . ', ' . + Json::encode($this->staticRows) . ', ' . ');'); $columnsField = $view->renderTemplate('_components/fieldtypes/Table/columntable.twig', [ @@ -358,6 +360,7 @@ public function getSettingsHtml(): ?string 'cols' => $columns, 'rows' => $this->defaults, 'initJs' => false, + 'includeRowId' => true, ]); return $view->renderTemplate('_components/fieldtypes/Table/settings.twig', [ @@ -367,6 +370,25 @@ public function getSettingsHtml(): ?string ]); } + /** + * @inheritdoc + */ + public function beforeSave(bool $isNew): bool + { + if (!parent::beforeSave($isNew)) { + return false; + } + + if ($this->staticRows && !empty($this->defaults)) { + // make sure the default rows have IDs assigned + foreach ($this->defaults as $key => $value) { + $this->defaults[$key]['rowId'] ??= $key; + } + } + + return true; + } + /** * @inheritdoc */ @@ -464,11 +486,60 @@ private function _normalizeValueInternal(mixed $value, ?ElementInterface $elemen $value = array_values($value); if ($this->staticRows) { + // get the order of the default rows + $order = ArrayHelper::getColumn($this->defaults, 'rowId'); + $missingValueRowIds = null; + + if (!empty($order)) { + // if there's no rowIds, add them + if (ArrayHelper::containsRecursive($value, 'rowId') === false) { + foreach ($value as $key => &$row) { + $row['rowId'] = $order[$key]; + } + } + + // the rowIds present in the $value array + $usedValueRowIds = ArrayHelper::getColumn($value, 'rowId'); + + // if the field has a set order + $missingValueRowIds = array_values(array_diff($order, $usedValueRowIds)); + $leftoverValueRowIds = array_diff($usedValueRowIds, $order); + + // if the rowId is missing from the defaults - remove it from the $value array + if (!empty($leftoverValueRowIds)) { + foreach ($leftoverValueRowIds as $key => $rowId) { + unset($value[$key]); + } + } + } + $valueRows = count($value); $totalRows = count($defaults); + + // if we have too few rows if ($valueRows < $totalRows) { - $value = array_pad($value, $totalRows, []); - } elseif ($valueRows > $totalRows) { + if ($missingValueRowIds === null) { + $value = array_pad($value, $totalRows, []); + } else { + // if we have the missing value rowIds - add them in places where settings rowId doesn't exist in the $value array + while (count($value) < $totalRows) { + $value[] = ['rowId' => reset($missingValueRowIds)]; + array_shift($missingValueRowIds); + } + } + } + + if (!empty($order)) { + // sort as per the field's settings + usort($value, function($a, $b) use ($order) { + $posA = array_search($a['rowId'], $order); + $posB = array_search($b['rowId'], $order); + return $posA - $posB; + }); + } + + // now that we've sorted the rows, if we have too many rows - splice + if ($valueRows > $totalRows) { array_splice($value, $totalRows); } } @@ -566,6 +637,14 @@ public function serializeValueForDb(mixed $value, ElementInterface $element): mi $serializedRow[$colId] = parent::serializeValue($value, $element); } } + + // if the table has static rows, store the rowId too + if ($this->staticRows) { + if (isset($row['rowId'])) { + $serializedRow['rowId'] = $row['rowId']; + } + } + $serialized[] = $serializedRow; } @@ -779,6 +858,7 @@ private function _getInputHtml(mixed $value, ?ElementInterface $element, bool $s 'allowReorder' => true, 'addRowLabel' => Craft::t('site', $this->addRowLabel), 'describedBy' => $this->describedBy, + 'includeRowId' => $this->staticRows, ]); } } diff --git a/src/templates/_includes/forms/editableTable.twig b/src/templates/_includes/forms/editableTable.twig index b5159d923e2..5289a67f1ad 100644 --- a/src/templates/_includes/forms/editableTable.twig +++ b/src/templates/_includes/forms/editableTable.twig @@ -6,6 +6,7 @@ {%- set minRows = minRows ?? null %} {%- set maxRows = maxRows ?? null %} {%- set describedBy = describedBy ?? null %} +{%- set includeRowId = includeRowId ?? false %} {%- set totalRows = rows|length %} {%- set staticRows = static or (staticRows ?? false) or (minRows == 1 and maxRows == 1 and totalRows == 1) %} @@ -71,6 +72,9 @@ {% if allowDelete %}{% endif %} {% if allowReorder %}{% endif %} {% endif %} + {% if includeRowId %} + {# for hidden row ID #} + {% endif %} {% if cols|filter(c => (c.headingHtml ?? c.heading ?? c.info ?? '') is not same as(''))|length %} @@ -92,12 +96,16 @@ {% if (allowDelete or allowReorder) %} {{ 'Row actions'|t('app') }} {% endif %} + {% if includeRowId %} + Row ID + {% endif %} {% endif %} {% for rowId, row in rows %} {% set rowNumber = loop.index %} + {% set rowIndex = loop.index0 %} {% for colId, col in cols %} {% set cell = row[colId] is defined ? row[colId] : (defaultValues[colId] ?? null) %} @@ -236,6 +244,9 @@ }) }} {%- endif -%} + {% if includeRowId %} + + {% endif %} {% endfor %} diff --git a/src/web/assets/cp/dist/cp.js b/src/web/assets/cp/dist/cp.js index 37495055fc3..5277f3ed6e1 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__={0:function(){function t(t,i){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var i=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=i){var n,s,a,r,o=[],l=!0,h=!1;try{if(a=(i=i.call(t)).next,0===e){if(Object(i)!==i)return;l=!1}else for(;!(l=(n=a.call(i)).done)&&(o.push(n.value),o.length!==e);l=!0);}catch(t){h=!0,s=t}finally{try{if(!l&&null!=i.return&&(r=i.return(),Object(r)!==r))return}finally{if(h)throw s}}return o}}(t,i)||function(t,i){if(t){if("string"==typeof t)return e(t,i);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?e(t,i):void 0}}(t,i)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,n=Array(e);ii.settings.maxFileSize&&(i._rejectedFiles.size.push("“"+t.name+"”"),s=!1),s&&"function"==typeof i.settings.canAddMoreFiles&&!i.settings.canAddMoreFiles(i._validFileCounter)&&(i._rejectedFiles.limit.push("“"+t.name+"”"),s=!1),s&&(i._validFileCounter++,e.submit()),++i._totalFileCounter===e.originalFiles.length&&(i._totalFileCounter=0,i._validFileCounter=0,i.processErrorMessages())})),!0},destroy:function(){var e=this;this.uploader.fileupload("instance")&&this.uploader.fileupload("destroy"),this.$element.off("fileuploadadd",this._onFileAdd),Object.entries(this.events).forEach((function(i){var n=t(i,2),s=n[0],a=n[1];e.$element.off(s,a)}))}},{defaults:{autoUpload:!1,sequentialUploads:!0,maxFileSize:Craft.maxUploadSize,replaceFileInput:!1,createAction:"assets/upload",replaceAction:"assets/replace-file",deleteAction:"assets/delete-asset"}})},9:function(){Craft.Structure=Garnish.Base.extend({id:null,$container:null,state:null,structureDrag:null,init:function(t,e,i){this.id=t,this.$container=$(e),this.setSettings(i,Craft.Structure.defaults),this.$container.data("structure")&&(console.warn("Double-instantiating a structure on an element"),this.$container.data("structure").destroy()),this.$container.data("structure",this),this.state={},this.settings.storageKey&&$.extend(this.state,Craft.getLocalStorage(this.settings.storageKey,{})),void 0===this.state.collapsedElementIds&&(this.state.collapsedElementIds=[]);for(var n=this.$container.find("ul").prev(".row"),s=0;s').prependTo(a);-1!==$.inArray(a.children(".element").data("id"),this.state.collapsedElementIds)&&r.addClass("collapsed"),this.initToggle(o)}this.settings.sortable&&(this.structureDrag=new Craft.StructureDrag(this,this.settings.maxLevels)),this.settings.newChildUrl&&this.initNewChildMenus(this.$container.find(".add"))},initToggle:function(t){var e=this;t.on("click",(function(t){var i=$(t.currentTarget).closest("li"),n=i.children(".row").find(".element:first").data("id"),s=$.inArray(n,e.state.collapsedElementIds);i.hasClass("collapsed")?(i.removeClass("collapsed"),-1!==s&&e.state.collapsedElementIds.splice(s,1)):(i.addClass("collapsed"),-1===s&&e.state.collapsedElementIds.push(n)),e.settings.storageKey&&Craft.setLocalStorage(e.settings.storageKey,e.state)}))},initNewChildMenus:function(t){this.addListener(t,"click","onNewChildMenuClick")},onNewChildMenuClick:function(t){var e=$(t.currentTarget);if(!e.data("menubtn")){var i=e.parent().children(".element").data("id"),n=Craft.getUrl(this.settings.newChildUrl,"parentId="+i);$('").insertAfter(e),new Garnish.MenuBtn(e).showMenu()}},getIndent:function(t){return Craft.Structure.baseIndent+(t-1)*Craft.Structure.nestedIndent},addElement:function(t){var e=$('
  • ').appendTo(this.$container),i=$('
    ').appendTo(e);if(i.append(t),this.settings.sortable&&(i.append(''),this.structureDrag.addItems(e)),this.settings.newChildUrl){var n=$('').appendTo(i);this.initNewChildMenus(n)}i.css("margin-bottom",-30),i.velocity({"margin-bottom":0},"fast")},removeElement:function(t){var e,i=this,n=t.parent().parent();this.settings.sortable&&this.structureDrag.removeItems(n),n.siblings().length||(e=n.parent()),n.css("visibility","hidden").velocity({marginBottom:-n.height()},"fast",(function(){n.remove(),void 0!==e&&i._removeUl(e)}))},_removeUl:function(t){t.siblings(".row").children(".toggle").remove(),t.remove()},destroy:function(){this.$container.removeData("structure"),this.base()}},{baseIndent:8,nestedIndent:35,defaults:{storageKey:null,sortable:!1,newChildUrl:null,maxLevels:null}})},258: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=$('
  • ").appendTo(n);var a=new Garnish.MenuBtn(this.$selectTransformBtn,{onOptionSelect:this.onSelectTransform.bind(this)});a.disable(),this.$selectTransformBtn.data("menuButton",a)}},onSelectionChange:function(t){var e=this.elementIndex.getSelectedElements(),i=!1;if(e.length&&this.settings.transforms.length){i=!0;for(var n=0;nt.length)&&(e=t.length);for(var i=0,n=Array(e);i').appendTo(Garnish.$bod);this.$sidebar=$('
    ').appendTo(n).attr({role:"navigation","aria-label":Craft.t("app","Source")}),this.$sourcesContainer=$('
    ').appendTo(this.$sidebar),this.$sourceSettingsContainer=$('
    ').appendTo(n),this.$footer=$('