From 517c588ef7224941af761ba4df4f329dd0a442db Mon Sep 17 00:00:00 2001 From: Klaas Poortinga Date: Fri, 17 Jul 2020 11:12:41 +0200 Subject: [PATCH 1/4] Fix filter type "group" when 500+ options are available (#5141) When 500 options or more are presented in a group filter, PHP `max_input_vars` limits may prevent the filter from working. This fix passes selected options through as a JSON string to get around the limits. --- modules/backend/widgets/Filter.php | 15 +++++++---- modules/system/assets/ui/js/filter.js | 39 +++++++++++++++++---------- modules/system/assets/ui/storm-min.js | 26 +++++++++--------- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/modules/backend/widgets/Filter.php b/modules/backend/widgets/Filter.php index 5e97706ac..7b3a8b40e 100644 --- a/modules/backend/widgets/Filter.php +++ b/modules/backend/widgets/Filter.php @@ -208,7 +208,8 @@ class Filter extends WidgetBase switch ($scope->type) { case 'group': - $active = $this->optionsFromAjax(post('options.active')); + $data = json_decode(post('options'), true); + $active = $this->optionsFromAjax($data ?: null); $this->setScopeValue($scope, $active); break; @@ -223,7 +224,8 @@ class Filter extends WidgetBase break; case 'date': - $dates = $this->datesFromAjax(post('options.dates')); + $data = json_decode(post('options'), true); + $dates = $this->datesFromAjax($data['dates'] ?? null); if (!empty($dates)) { list($date) = $dates; @@ -236,7 +238,8 @@ class Filter extends WidgetBase break; case 'daterange': - $dates = $this->datesFromAjax(post('options.dates')); + $data = json_decode(post('options'), true); + $dates = $this->datesFromAjax($data['dates'] ?? null); if (!empty($dates)) { list($after, $before) = $dates; @@ -251,7 +254,8 @@ class Filter extends WidgetBase break; case 'number': - $numbers = $this->numbersFromAjax(post('options.numbers')); + $data = json_decode(post('options'), true); + $numbers = $this->numbersFromAjax($data['numbers'] ?? null); if (!empty($numbers)) { list($number) = $numbers; @@ -264,7 +268,8 @@ class Filter extends WidgetBase break; case 'numberrange': - $numbers = $this->numbersFromAjax(post('options.numbers')); + $data = json_decode(post('options'), true); + $numbers = $this->numbersFromAjax($data['numbers'] ?? null); if (!empty($numbers)) { list($min, $max) = $numbers; diff --git a/modules/system/assets/ui/js/filter.js b/modules/system/assets/ui/js/filter.js index 708efa294..8c9563365 100644 --- a/modules/system/assets/ui/js/filter.js +++ b/modules/system/assets/ui/js/filter.js @@ -23,6 +23,7 @@ this.options = options || {} this.scopeValues = {} + this.scopeAvailable = {} this.$activeScope = null this.activeScopeName = null this.isActiveScopeDirty = false @@ -286,23 +287,24 @@ var itemId = $item.data('item-id'), - items = this.scopeValues[this.activeScopeName], - fromItems = isDeselect ? items.active : items.available, - toItems = isDeselect ? items.available : items.active, - testFunc = function(item){ return item.id == itemId }, + active = this.scopeValues[this.activeScopeName], + available = this.scopeAvailable[this.activeScopeName], + fromItems = isDeselect ? active : available, + toItems = isDeselect ? available : active, + testFunc = function(active){ return active.id == itemId }, item = $.grep(fromItems, testFunc).pop(), filtered = $.grep(fromItems, testFunc, true) if (isDeselect) - this.scopeValues[this.activeScopeName].active = filtered + this.scopeValues[this.activeScopeName] = filtered else - this.scopeValues[this.activeScopeName].available = filtered + this.scopeAvailable[this.activeScopeName] = filtered if (item) toItems.push(item) - this.toggleFilterButtons(items) - this.updateScopeSetting(this.$activeScope, items.active.length) + this.toggleFilterButtons(active) + this.updateScopeSetting(this.$activeScope, isDeselect ? filtered.length : active.length) this.isActiveScopeDirty = true this.focusSearch() } @@ -310,10 +312,17 @@ FilterWidget.prototype.displayPopover = function($scope) { var self = this, scopeName = $scope.data('scope-name'), - data = this.scopeValues[scopeName], + data = null, isLoaded = true, container = false + if (typeof this.scopeAvailable[scopeName] !== "undefined" && this.scopeAvailable[scopeName]) { + data = $.extend({}, data, { + available: this.scopeAvailable[scopeName], + active: this.scopeValues[scopeName] + }) + } + // If the filter is running in a modal, popovers should be // attached to the modal container. This prevents z-index issues. var modalParent = $scope.parents('.modal-dialog') @@ -390,7 +399,8 @@ if (!data.active) data.active = [] if (!data.available) data.available = [] - this.scopeValues[scopeName] = data + this.scopeValues[scopeName] = data.active; + this.scopeAvailable[scopeName] = data.available; // Do not render if scope has changed if (scopeName != this.activeScopeName) @@ -424,9 +434,9 @@ /* * Ensure any active items do not appear in the search results */ - if (items.active.length) { + if (items.length) { var activeIds = [] - $.each(items.active, function (key, obj) { + $.each(items, function (key, obj) { activeIds.push(obj.id) }) @@ -457,7 +467,7 @@ buttonContainer = $('#controlFilterPopover .filter-buttons') if (data) { - data.active.length > 0 ? buttonContainer.show() : buttonContainer.hide() + data.length > 0 ? buttonContainer.show() : buttonContainer.hide() } else { items.children().length > 0 ? buttonContainer.show() : buttonContainer.hide() } @@ -473,7 +483,7 @@ var self = this, data = { scopeName: scopeName, - options: this.scopeValues[scopeName] + options: JSON.stringify(this.scopeValues[scopeName]) } $.oc.stripeLoadIndicator.show() @@ -541,6 +551,7 @@ if (isReset) { this.scopeValues[scopeName] = null + this.scopeAvailable[scopeName] = null this.updateScopeSetting(this.$activeScope, 0) } diff --git a/modules/system/assets/ui/storm-min.js b/modules/system/assets/ui/storm-min.js index 8803506a3..632ec830d 100644 --- a/modules/system/assets/ui/storm-min.js +++ b/modules/system/assets/ui/storm-min.js @@ -3044,6 +3044,7 @@ $.fn.toolbar.noConflict=function(){$.fn.toolbar=old return this} $(document).on('render',function(){$('[data-control=toolbar]').toolbar()})}(window.jQuery);+function($){"use strict";var FilterWidget=function(element,options){this.$el=$(element);this.options=options||{} this.scopeValues={} +this.scopeAvailable={} this.$activeScope=null this.activeScopeName=null this.isActiveScopeDirty=false @@ -3148,18 +3149,19 @@ $item.addClass('animate-enter').prependTo($otherContainer).one('webkitAnimationE if(!this.scopeValues[this.activeScopeName]) return var -itemId=$item.data('item-id'),items=this.scopeValues[this.activeScopeName],fromItems=isDeselect?items.active:items.available,toItems=isDeselect?items.available:items.active,testFunc=function(item){return item.id==itemId},item=$.grep(fromItems,testFunc).pop(),filtered=$.grep(fromItems,testFunc,true) +itemId=$item.data('item-id'),active=this.scopeValues[this.activeScopeName],available=this.scopeAvailable[this.activeScopeName],fromItems=isDeselect?active:available,toItems=isDeselect?available:active,testFunc=function(active){return active.id==itemId},item=$.grep(fromItems,testFunc).pop(),filtered=$.grep(fromItems,testFunc,true) if(isDeselect) -this.scopeValues[this.activeScopeName].active=filtered +this.scopeValues[this.activeScopeName]=filtered else -this.scopeValues[this.activeScopeName].available=filtered +this.scopeAvailable[this.activeScopeName]=filtered if(item) toItems.push(item) -this.toggleFilterButtons(items) -this.updateScopeSetting(this.$activeScope,items.active.length) +this.toggleFilterButtons(active) +this.updateScopeSetting(this.$activeScope,isDeselect?filtered.length:active.length) this.isActiveScopeDirty=true this.focusSearch()} -FilterWidget.prototype.displayPopover=function($scope){var self=this,scopeName=$scope.data('scope-name'),data=this.scopeValues[scopeName],isLoaded=true,container=false +FilterWidget.prototype.displayPopover=function($scope){var self=this,scopeName=$scope.data('scope-name'),data=null,isLoaded=true,container=false +if(typeof this.scopeAvailable[scopeName]!=="undefined"&&this.scopeAvailable[scopeName]){data=$.extend({},data,{available:this.scopeAvailable[scopeName],active:this.scopeValues[scopeName]})} var modalParent=$scope.parents('.modal-dialog') if(modalParent.length>0){container=modalParent[0]} if(!data){data={loading:true} @@ -3181,8 +3183,7 @@ FilterWidget.prototype.fillOptions=function(scopeName,data){if(this.scopeValues[ return if(!data.active)data.active=[] if(!data.available)data.available=[] -this.scopeValues[scopeName]=data -if(scopeName!=this.activeScopeName) +this.scopeValues[scopeName]=data.active;this.scopeAvailable[scopeName]=data.available;if(scopeName!=this.activeScopeName) return var container=$('#controlFilterPopover .filter-items > ul').empty() this.addItemsToListElement(container,data.available) @@ -3194,8 +3195,8 @@ if(!this.scopeValues[this.activeScopeName]) return var self=this,filtered=[],items=this.scopeValues[scopeName] -if(items.active.length){var activeIds=[] -$.each(items.active,function(key,obj){activeIds.push(obj.id)}) +if(items.length){var activeIds=[] +$.each(items,function(key,obj){activeIds.push(obj.id)}) filtered=$.grep(available,function(item){return $.inArray(item.id,activeIds)===-1})} else{filtered=available} var container=$('#controlFilterPopover .filter-items > ul').empty() @@ -3204,10 +3205,10 @@ FilterWidget.prototype.addItemsToListElement=function($ul,items){$.each(items,fu $ul.append(item)})} FilterWidget.prototype.toggleFilterButtons=function(data) {var items=$('#controlFilterPopover .filter-active-items > ul'),buttonContainer=$('#controlFilterPopover .filter-buttons') -if(data){data.active.length>0?buttonContainer.show():buttonContainer.hide()}else{items.children().length>0?buttonContainer.show():buttonContainer.hide()}} +if(data){data.length>0?buttonContainer.show():buttonContainer.hide()}else{items.children().length>0?buttonContainer.show():buttonContainer.hide()}} FilterWidget.prototype.pushOptions=function(scopeName){if(!this.isActiveScopeDirty||!this.options.updateHandler) return -var self=this,data={scopeName:scopeName,options:this.scopeValues[scopeName]} +var self=this,data={scopeName:scopeName,options:JSON.stringify(this.scopeValues[scopeName])} $.oc.stripeLoadIndicator.show() this.$el.request(this.options.updateHandler,{data:data}).always(function(){$.oc.stripeLoadIndicator.hide()}).done(function(){self.$el.find('[data-scope-name="'+scopeName+'"]').trigger('change.oc.filterScope')})} FilterWidget.prototype.checkboxToggle=function($el){var isChecked=$el.is(':checked'),$scope=$el.closest('.filter-scope'),scopeName=$scope.data('scope-name') @@ -3224,6 +3225,7 @@ this.$el.request(this.options.updateHandler,{data:data}).always(function(){$.oc. $scope.toggleClass('active',!!switchValue)} FilterWidget.prototype.filterScope=function(isReset){var scopeName=this.$activeScope.data('scope-name') if(isReset){this.scopeValues[scopeName]=null +this.scopeAvailable[scopeName]=null this.updateScopeSetting(this.$activeScope,0)} this.pushOptions(scopeName);this.isActiveScopeDirty=true;this.$activeScope.data('oc.popover').hide()} FilterWidget.prototype.getLang=function(name,defaultValue){if($.oc===undefined||$.oc.lang===undefined){return defaultValue} From c1fd1b9346e819ea5f3fe3a84ee24fcd6da9599f Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 19 Jul 2020 01:01:09 -0600 Subject: [PATCH 2/4] Fix support for ignoreTimezone in date filter types Fixes #5197 --- modules/backend/widgets/filter/partials/_scope_date.htm | 2 +- modules/backend/widgets/filter/partials/_scope_daterange.htm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/backend/widgets/filter/partials/_scope_date.htm b/modules/backend/widgets/filter/partials/_scope_date.htm index 64701fd13..b8a019178 100644 --- a/modules/backend/widgets/filter/partials/_scope_date.htm +++ b/modules/backend/widgets/filter/partials/_scope_date.htm @@ -10,7 +10,7 @@ 'firstDay' => $scope->firstDay, 'yearRange' => $scope->yearRange, ])) ?>" - ignoreTimezone ? 'data-ignore-timezone' : ''; ?> + ignoreTimezone ? 'data-ignore-timezone' : ''; ?> > label)) ?>: diff --git a/modules/backend/widgets/filter/partials/_scope_daterange.htm b/modules/backend/widgets/filter/partials/_scope_daterange.htm index 00cc7c77d..9c336385b 100644 --- a/modules/backend/widgets/filter/partials/_scope_daterange.htm +++ b/modules/backend/widgets/filter/partials/_scope_daterange.htm @@ -10,7 +10,7 @@ 'firstDay' => $scope->firstDay, 'yearRange' => $scope->yearRange, ])) ?>" - ignoreTimezone ? 'data-ignore-timezone' : ''; ?> + ignoreTimezone ? 'data-ignore-timezone' : ''; ?> > label)) ?>: From 5a5208bd0b75c411840d4246bd5b3df427005ced Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 19 Jul 2020 01:08:01 -0600 Subject: [PATCH 3/4] Document caveat with uploaded file URL generation when installing October in a subfolder Fixes #5204 --- config/cms.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/cms.php b/config/cms.php index 4c838a026..790355414 100644 --- a/config/cms.php +++ b/config/cms.php @@ -298,6 +298,16 @@ return [ | in cloud storage (ex. AWS, RackSpace) are valid for by setting | temporaryUrlTTL to a value in seconds to define a validity period. This | is only used for the 'uploads' config when using a supported cloud disk + | + | NOTE: If you have installed October in a subfolder, are using local + | storage and are not using a linkPolicy of 'force' you should include + | the path to the subfolder in the `path` option for these storage + | configurations. + | + | Example: October is installed under https://localhost/projects/october. + | You should then specify `/projects/october/storage/app/uploads` as the + | path for the uploads disk and `/projects/october/storage/app/media` as + | the path for the media disk. */ 'storage' => [ From a56e0cdf616b35de76f36c005df147ece312c933 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 19 Jul 2020 01:15:07 -0600 Subject: [PATCH 4/4] Use Arabic numerals instead of Indic ones for Arabic date translations. Fixes #5213 --- modules/system/assets/js/lang/lang.ar.js | 36 +++++++------------ .../assets/ui/vendor/moment/locale/ar.js | 36 +++++++------------ 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/modules/system/assets/js/lang/lang.ar.js b/modules/system/assets/js/lang/lang.ar.js index bc6151182..cc1f2223e 100644 --- a/modules/system/assets/js/lang/lang.ar.js +++ b/modules/system/assets/js/lang/lang.ar.js @@ -9,6 +9,7 @@ $.oc.langMessages['ar'] = $.extend( ); //! moment.js locale configuration v2.22.2 +//!!! IMPORTANT - modified from default - see https://github.com/octobercms/october/issues/5213 ;(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' @@ -19,27 +20,16 @@ $.oc.langMessages['ar'] = $.extend( var symbolMap = { - '1': '١', - '2': '٢', - '3': '٣', - '4': '٤', - '5': '٥', - '6': '٦', - '7': '٧', - '8': '٨', - '9': '٩', - '0': '٠' - }, numberMap = { - '١': '1', - '٢': '2', - '٣': '3', - '٤': '4', - '٥': '5', - '٦': '6', - '٧': '7', - '٨': '8', - '٩': '9', - '٠': '0' + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + '6': '6', + '7': '7', + '8': '8', + '9': '9', + '0': '0' }, pluralForm = function (n) { return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5; }, plurals = { @@ -124,9 +114,7 @@ $.oc.langMessages['ar'] = $.extend( yy : pluralize('y') }, preparse: function (string) { - return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { - return numberMap[match]; - }).replace(/،/g, ','); + return string.replace(/،/g, ','); }, postformat: function (string) { return string.replace(/\d/g, function (match) { diff --git a/modules/system/assets/ui/vendor/moment/locale/ar.js b/modules/system/assets/ui/vendor/moment/locale/ar.js index e0d3c98f1..fed1c9cdc 100644 --- a/modules/system/assets/ui/vendor/moment/locale/ar.js +++ b/modules/system/assets/ui/vendor/moment/locale/ar.js @@ -1,4 +1,5 @@ //! moment.js locale configuration v2.22.2 +//!!! IMPORTANT - modified from default - see https://github.com/octobercms/october/issues/5213 ;(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' @@ -9,27 +10,16 @@ var symbolMap = { - '1': '١', - '2': '٢', - '3': '٣', - '4': '٤', - '5': '٥', - '6': '٦', - '7': '٧', - '8': '٨', - '9': '٩', - '0': '٠' - }, numberMap = { - '١': '1', - '٢': '2', - '٣': '3', - '٤': '4', - '٥': '5', - '٦': '6', - '٧': '7', - '٨': '8', - '٩': '9', - '٠': '0' + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + '6': '6', + '7': '7', + '8': '8', + '9': '9', + '0': '0' }, pluralForm = function (n) { return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5; }, plurals = { @@ -114,9 +104,7 @@ yy : pluralize('y') }, preparse: function (string) { - return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { - return numberMap[match]; - }).replace(/،/g, ','); + return string.replace(/،/g, ','); }, postformat: function (string) { return string.replace(/\d/g, function (match) {