From 400940ee7457df1157c4791f2a46617e71bab075 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 31 Oct 2024 02:24:29 -0400 Subject: [PATCH 01/39] Update utilities.js Formatting, style, optimization only --- manager/assets/modext/util/utilities.js | 1141 ++++++++++++++++------- 1 file changed, 778 insertions(+), 363 deletions(-) diff --git a/manager/assets/modext/util/utilities.js b/manager/assets/modext/util/utilities.js index 0037176002..555d2dbc2f 100644 --- a/manager/assets/modext/util/utilities.js +++ b/manager/assets/modext/util/utilities.js @@ -8,78 +8,75 @@ Ext.namespace('MODx.util.Format'); * @param {Object} config An object of configuration properties * @xtype modx-json-reader */ -MODx.util.JSONReader = function(config) { - config = config || {}; - Ext.applyIf(config,{ - successProperty:'success' - ,totalProperty: 'total' - ,root: 'data' +MODx.util.JSONReader = function(config = {}) { + Ext.applyIf(config, { + successProperty: 'success', + totalProperty: 'total', + root: 'data' }); - MODx.util.JSONReader.superclass.constructor.call(this,config,['id','msg']); + MODx.util.JSONReader.superclass.constructor.call(this, config, ['id', 'msg']); }; -Ext.extend(MODx.util.JSONReader,Ext.data.JsonReader); -Ext.reg('modx-json-reader',MODx.util.JSONReader); +Ext.extend(MODx.util.JSONReader, Ext.data.JsonReader); +Ext.reg('modx-json-reader', MODx.util.JSONReader); /** * @class MODx.util.Progress */ MODx.util.Progress = { - id: 0 - ,time: function(v,id,msg) { + id: 0, + time: function(v, id, msg) { msg = msg || _('saving'); if (MODx.util.Progress.id === id && v < 11) { - Ext.MessageBox.updateProgress(v/10,msg); + Ext.MessageBox.updateProgress(v / 10, msg); } - } - ,reset: function() { - MODx.util.Progress.id = MODx.util.Progress.id + 1; + }, + reset: function() { + MODx.util.Progress.id += 1; } }; - MODx.util.UrlParams = { get() { - return this.parse(window.location.search) + return this.parse(window.location.search); }, set(data) { - const params = decodeURIComponent(new URLSearchParams(data).toString()) + const params = decodeURIComponent(new URLSearchParams(data).toString()); if (params.length) { - window.history.pushState(params, '', document.location.pathname + '?' + params); + window.history.pushState(params, '', `${document.location.pathname}?${params}`); } else { window.history.pushState('', '', document.location.pathname); } }, add(key, val) { - const params = this.get() - params[key] = val - this.set(params) + const params = this.get(); + params[key] = val; + this.set(params); }, remove(key) { - const params = this.get() - delete params[key] - this.set(params) + const params = this.get(); + delete params[key]; + this.set(params); }, clear() { - this.set({}) + this.set({}); }, parse(str) { - const params = new URLSearchParams(str) - return Object.fromEntries(params.entries()) + const params = new URLSearchParams(str); + return Object.fromEntries(params.entries()); } -} +}; /** Adds a lock mask to an element */ -MODx.LockMask = function(config) { - config = config || {}; - Ext.applyIf(config,{ - msg: _('locked') - ,msgCls: 'modx-lockmask' +MODx.LockMask = function(config = {}) { + Ext.applyIf(config, { + msg: _('locked'), + msgCls: 'modx-lockmask' }); - MODx.LockMask.superclass.constructor.call(this,config.el,config); + MODx.LockMask.superclass.constructor.call(this, config.el, config); }; -Ext.extend(MODx.LockMask,Ext.LoadMask,{ - locked: false - ,toggle: function() { +Ext.extend(MODx.LockMask, Ext.LoadMask, { + locked: false, + toggle: function() { if (this.locked) { this.hide(); this.locked = false; @@ -87,20 +84,26 @@ Ext.extend(MODx.LockMask,Ext.LoadMask,{ this.show(); this.locked = true; } + }, + lock: function() { + this.locked = true; + this.show(); + }, + unlock: function() { + this.locked = false; + this.hide(); } - ,lock: function() { this.locked = true; this.show(); } - ,unlock: function() { this.locked = false; this.hide(); } }); -Ext.reg('modx-lockmask',MODx.LockMask); +Ext.reg('modx-lockmask', MODx.LockMask); /** * Adds a new config parameter to allow preservation of trailing zeros in decimal numbers */ Ext.override(Ext.form.NumberField, { strictDecimalPrecision: false, - fixPrecision : function(value){ - var nan = isNaN(value); - if(!this.allowDecimals || this.decimalPrecision == -1 || nan || !value){ + fixPrecision: function(value) { + const nan = Number.isNaN(value); + if (!this.allowDecimals || this.decimalPrecision === -1 || nan || !value) { return nan ? '' : value; } return this.allowDecimals && this.strictDecimalPrecision @@ -111,22 +114,22 @@ Ext.override(Ext.form.NumberField, { }); /** add clearDirty to basicform */ -Ext.override(Ext.form.BasicForm,{ - clearDirty : function(nodeToRecurse){ +Ext.override(Ext.form.BasicForm, { + clearDirty: function(nodeToRecurse) { nodeToRecurse = nodeToRecurse || this; - nodeToRecurse?.items?.each?.(function(f){ - if (!f.getValue) return; - - if(f.items){ - this.clearDirty(f); - } else if(f.originalValue != f.getValue()){ - f.originalValue = f.getValue(); + nodeToRecurse?.items?.each?.(function(field) { + if (!field.getValue) { + return; } - },this); + if (field.items) { + this.clearDirty(field); + } else if (field.originalValue !== field.getValue()) { + field.originalValue = field.getValue(); + } + }, this); } }); - /** * Static Textfield */ @@ -139,7 +142,7 @@ MODx.StaticTextField = Ext.extend(Ext.form.TextField, { MODx.StaticTextField.superclass.onRender.apply(this, arguments); } }); -Ext.reg('statictextfield',MODx.StaticTextField); +Ext.reg('statictextfield', MODx.StaticTextField); /** * Static Boolean @@ -151,10 +154,10 @@ MODx.StaticBoolean = Ext.extend(Ext.form.TextField, { this.readOnly = true; this.disabled = !this.initialConfig.submitValue; MODx.StaticBoolean.superclass.onRender.apply(this, arguments); - this.on('change',this.onChange,this); - } + this.on('change', this.onChange, this); + }, - ,setValue: function(v) { + setValue: function(v) { if (v === 1) { this.addClass('green'); v = _('yes'); @@ -165,33 +168,38 @@ MODx.StaticBoolean = Ext.extend(Ext.form.TextField, { MODx.StaticBoolean.superclass.setValue.apply(this, arguments); } }); -Ext.reg('staticboolean',MODx.StaticBoolean); +Ext.reg('staticboolean', MODx.StaticBoolean); // This method strips not allowed html tags/attributes, html comments and php tags, // replaces javascript invocation in a href attribute and masks html event attributes // in an input string - assuming the result is safe to be displayed by a browser -MODx.util.safeHtml = function (input, allowedTags, allowedAttributes) { - var strip = function(input, allowedTags, allowedAttributes) { - return input.replace(tags, function ($0, $1) { - return allowedTags.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; - }).replace(attributes, function ($0, $1) { - return allowedAttributes.indexOf($1.toLowerCase() + ',') > -1 ? $0 : ''; - }); - }; - allowedTags = (((allowedTags || '
') + '') - .toLowerCase() - .match(/<[a-z][a-z0-9]*>/g) || []) - .join(''); // making sure the allowedTags arg is a string containing only tags in lowercase (
) - allowedAttributes = (((allowedAttributes || 'href,class') + '') - .toLowerCase() - .match(/[a-z\-,]*/g) || []) - .join('').concat(','); // making sure the allowedAttributes arg is a comma separated string containing only attributes in lowercase (a,b,c) - var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, +MODx.util.safeHtml = (input, allowedTags, allowedAttributes) => { + const + tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, attributes = /([a-z][a-z0-9]*)\s*=\s*".*?"/gi, eventAttributes = /on([a-z][a-z0-9]*\s*=)/gi, commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi, hrefJavascript = /href(\s*?=\s*?(["'])javascript:.*?\2|\s*?=\s*?javascript:.*?(?![^> ]))/gi, - length; + strip = (string, allowedTagsDef, allowedAttrDef) => { + const + tagsReplacer = (match, group1) => (allowedTagsDef.indexOf(`<${group1.toLowerCase()}>`) > -1 ? match : ''), + attrReplacer = (match, group1) => (allowedAttrDef.indexOf(`${group1.toLowerCase()},`) > -1 ? match : '') + ; + return string.replace(tags, tagsReplacer).replace(attributes, attrReplacer); + } + ; + let length; + // making sure the allowedTags arg is a string containing only tags in lowercase () + allowedTags = ((`${allowedTags || '
'}`) + .toLowerCase() + .match(/<[a-z][a-z0-9]*>/g) || []) + .join(''); + // making sure the allowedAttributes arg is a comma separated string containing only attributes in lowercase (a,b,c) + allowedAttributes = ((`${allowedAttributes || 'href,class'}`) + .toLowerCase() + .match(/[a-z\-,]*/g) || []) + .join('') + .concat(','); input = input.replace(commentsAndPhpTags, '').replace(hrefJavascript, 'href="javascript:void(0)"'); do { length = input.length; @@ -200,226 +208,247 @@ MODx.util.safeHtml = function (input, allowedTags, allowedAttributes) { return input.replace(eventAttributes, 'on​$1'); }; -/**************************************************************************** - * Ext-specific overrides/extensions * - ****************************************************************************/ +// *** Ext-specific overrides/extensions *** /* add helper method to set checkbox boxLabel */ Ext.override(Ext.form.Checkbox, { - setBoxLabel: function(boxLabel){ + setBoxLabel: function(boxLabel) { this.boxLabel = boxLabel; - if(this.rendered){ + if (this.rendered) { this.wrap.child('.x-form-cb-label').update(boxLabel); } } }); -var FieldSetonRender = Ext.form.FieldSet.prototype.onRender; -Ext.override(Ext.form.FieldSet, { - onRender : function(ct, position){ - FieldSetonRender.call(this, ct, position); +const FieldSetOnRender = Ext.form.FieldSet.prototype.onRender; - if(this.checkboxToggle){ - var trigger = this.el.dom.getElementsByClassName(this.headerTextCls)[0]; - var elem = this; +Ext.override(Ext.form.FieldSet, { + onRender: function(ct, position) { + FieldSetOnRender.call(this, ct, position); + if (this.checkboxToggle) { + const + trigger = this.el.dom.getElementsByClassName(this.headerTextCls)[0], + elem = this + ; if (trigger) { - trigger.addEventListener('click', function(e) { + trigger.addEventListener('click', e => { elem.checkbox.dom.click(e); }, false); } } - }, + } }); - -Array.prototype.in_array = function(p_val) { - for(var i=0,l=this.length;i 0) { + if (fields.length > 0) { this.items.addAll(fields); - for(var f=0;f', - elbowMarkup = n.attributes.pseudoroot ? - '' : - '', - - buf = ['
  • ', - '',this.indentMarkup,"", + iconClass = a.iconCls ? ` ${a.iconCls}` : '', + iconMarkup = ``, + elbowMarkup = n.attributes.pseudoroot + ? '' + : '', + checkboxMarkup = hasCheckbox ? (`' : '>'}`) : '', + targetMarkup = a.hrefTarget ? `target="${a.hrefTarget}"` : '', + buf = [ + '
  • ', + `", + checkboxMarkup, + ``, + `${renderer(a)}`, + '', + '', '', - "
  • "].join(''); - - if(bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())){ - this.wrap = Ext.DomHelper.insertHtml("beforeBegin", nel, buf); - }else{ - this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf); + '' + ].join('') + ; + let nel; + // eslint-disable-next-line no-cond-assign + if (bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())) { + this.wrap = Ext.DomHelper.insertHtml('beforeBegin', nel, buf); + } else { + this.wrap = Ext.DomHelper.insertHtml('beforeEnd', targetNode, buf); } + /* eslint-disable prefer-destructuring */ this.elNode = this.wrap.childNodes[0]; this.ctNode = this.wrap.childNodes[1]; - var cs = this.elNode.childNodes; + const cs = this.elNode.childNodes; this.indentNode = cs[0]; this.ecNode = cs[1]; this.iconNode = cs[2]; - var index = 3; - if(cb){ + let index = 3; + if (hasCheckbox) { this.checkbox = cs[3]; - - this.checkbox.defaultChecked = this.checkbox.checked; index++; } this.anchor = cs[index]; this.textNode = cs[index].firstChild; - } + /* eslint-enable prefer-destructuring */ + }, /** * Renders the item text as a XSS-safe value. Can be overridden with a renderItemText method on the Tree. * @param text * @returns string */ - ,renderItemText: function(item) { - return Ext.util.Format.htmlEncode(item.text) - } - ,getChildIndent : function(){ - if(!this.childIndent){ - var buf = [], - p = this.node; - while(p){ - if((!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)) && !p.attributes.pseudoroot){ - if(!p.isLast()) { - buf.unshift(''); + renderItemText: function(item) { + return Ext.util.Format.htmlEncode(item.text); + }, + getChildIndent: function() { + if (!this.childIndent) { + const buf = []; + let p = this.node; + while (p) { + if ((!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)) && !p.attributes.pseudoroot) { + if (!p.isLast()) { + buf.unshift(``); } else { - buf.unshift(''); + buf.unshift(``); } } p = p.parentNode; } - this.childIndent = buf.join(""); + this.childIndent = buf.join(''); } return this.childIndent; } }); - /* allows for messages in JSON responses */ -Ext.override(Ext.form.Action.Submit,{ - handleResponse : function(response){ - var m = Ext.decode(response.responseText); /* shaun 7/11/07 */ +Ext.override(Ext.form.Action.Submit, { + handleResponse: function(response) { + const messageData = Ext.decode(response.responseText); if (this.form.errorReader) { - var rs = this.form.errorReader.read(response); - var errors = []; - if (rs.records) { - for(var i = 0, len = rs.records.length; i < len; i=i+1) { - var r = rs.records[i]; - errors[i] = r.data; + const + responseData = this.form.errorReader.read(response), + errors = [] + ; + if (responseData.records) { + for (let i = 0, len = responseData.records.length; i < len; i++) { + const record = responseData.records[i]; + errors[i] = record.data; } } - if (errors.length < 1) { errors = null; } return { - success : rs.success - ,message : m.message /* shaun 7/11/07 */ - ,object : m.object /* shaun 7/18/07 */ - ,errors : errors + success: responseData.success, + message: messageData.message, + object: messageData.object, + errors: errors.length < 1 ? null : errors }; } return Ext.decode(response.responseText); @@ -428,40 +457,43 @@ Ext.override(Ext.form.Action.Submit,{ /* QTips to form fields */ Ext.form.Field.prototype.afterRender = Ext.form.Field.prototype.afterRender.createSequence(function() { - if (this.description && parseInt(MODx.config.manager_tooltip_enable)) { + if (this.description && parseInt(MODx.config.manager_tooltip_enable, 10)) { Ext.QuickTips.register({ - target: this.getEl() - ,text: this.description - ,enabled: true - ,dismissDelay: MODx.config.manager_tooltip_delay + target: this.getEl(), + text: this.description, + enabled: true, + dismissDelay: MODx.config.manager_tooltip_delay }); - var label = Ext.form.Field.findLabel(this); - if(label){ + const label = Ext.form.Field.findLabel(this); + if (label) { Ext.QuickTips.register({ - target: label - ,text: this.description - ,enabled: true - ,dismissDelay: MODx.config.manager_tooltip_delay + target: label, + text: this.description, + enabled: true, + dismissDelay: MODx.config.manager_tooltip_delay }); } } }); -Ext.applyIf(Ext.form.Field,{ + +Ext.applyIf(Ext.form.Field, { findLabel: function(field) { - var wrapDiv = null; - var label = null; + let + wrapDiv = null, + label = null + ; wrapDiv = field.getEl().up('div.x-form-element'); - if(wrapDiv){ + if (wrapDiv) { label = wrapDiv.child('label'); } - if(label){ + if (label) { return label; } wrapDiv = field.getEl().up('div.x-form-item'); - if(wrapDiv) { + if (wrapDiv) { label = wrapDiv.child('label'); } - if(label){ + if (label) { return label; } } @@ -469,23 +501,29 @@ Ext.applyIf(Ext.form.Field,{ MODx.util.Format = { dateFromTimestamp: function(timestamp, date, time, defaultValue) { - if (date === undefined) date = true; - if (time === undefined) time = true; - if (defaultValue === undefined) defaultValue = ''; - - timestamp = parseInt(timestamp); - if (!(timestamp > 0)) return defaultValue; - + if (date === undefined) { + date = true; + } + if (time === undefined) { + time = true; + } + if (defaultValue === undefined) { + defaultValue = ''; + } + timestamp = parseInt(timestamp, 10); + if (!(timestamp > 0)) { + return defaultValue; + } if (timestamp.toString().length === 10) { timestamp *= 1000; } - var format = []; + let format = []; - if (date === true) format.push(MODx.config.manager_date_format); - if (time === true) format.push(MODx.config.manager_time_format); + if (date === true) { format.push(MODx.config.manager_date_format); } + if (time === true) { format.push(MODx.config.manager_time_format); } - if (format.length === 0) return defaultValue; + if (format.length === 0) { return defaultValue; } format = format.join(' '); @@ -561,21 +599,23 @@ MODx.util.getHeaderBreadCrumbs = function(header, trail) { xtype: 'modx-header' }; } - - if (trail === undefined) trail = []; - if (!Array.isArray(trail)) trail = [trail]; - + if (trail === undefined) { + trail = []; + } + if (!Array.isArray(trail)) { + trail = [trail]; + } return { xtype: 'modx-breadcrumbs-panel', id: 'modx-header-breadcrumbs', cls: 'modx-header-breadcrumbs', desc: '', - bdMarkup: '
    • ' + - '{text}' + - '{text}' + - '
    ', + bdMarkup: '
    • ' + + '{text}' + + '{text}' + + '
    ', init: function() { - this.tpl = new Ext.XTemplate(this.bdMarkup, {compiled: true}); + this.tpl = new Ext.XTemplate(this.bdMarkup, { compiled: true }); }, trail: trail, listeners: { @@ -583,27 +623,26 @@ MODx.util.getHeaderBreadCrumbs = function(header, trail) { this.renderTrail(); } }, - renderTrail: function () { - this.tpl.overwrite(this.body.dom.lastElementChild, {trail: this.trail}); + renderTrail: function() { + this.tpl.overwrite(this.body.dom.lastElementChild, { trail: this.trail }); }, + // eslint-disable-next-line no-shadow updateTrail: function(trail, replace) { - if (replace === undefined) replace = false; - + if (replace === undefined) { + replace = false; + } if (replace === true) { this.trail = (Array.isArray(trail)) ? trail : [trail]; this.renderTrail(); return true; } - if (Array.isArray(trail)) { - for (var i = 0; i < trail.length; i++) { + for (let i = 0; i < trail.length; i++) { this.trail.push(trail[i]); } - this.renderTrail(); return true; } - this.trail.push(trail); this.renderTrail(); return true; @@ -737,71 +776,237 @@ MODx.util.tree = { }; Ext.util.Format.trimCommas = function(s) { - s = s.replace(',,',','); - var len = s.length; - if (s.substr(len-1,1) == ",") { - s = s.substring(0,len-1); + s = s.replace(',,', ','); + const len = s.length; + if (s.substr(len - 1, 1) === ',') { + s = s.substring(0, len - 1); } - if (s.substr(0,1) == ",") { + if (s.substr(0, 1) === ',') { s = s.substring(1); } - if (s == ',') { s = ''; } + if (s === ',') { + s = ''; + } return s; }; /* rowactions plugin */ -Ext.ns('Ext.ux.grid');if('function'!==typeof RegExp.escape){RegExp.escape=function(s){if('string'!==typeof s){return s}return s.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g,'\\$1')}}Ext.ux.grid.RowActions=function(a){Ext.apply(this,a);this.addEvents('beforeaction','action','beforegroupaction','groupaction');Ext.ux.grid.RowActions.superclass.constructor.call(this)};Ext.extend(Ext.ux.grid.RowActions,Ext.util.Observable,{actionEvent:'click',autoWidth:true,dataIndex:'',editable:false,header:'',isColumn:true,keepSelection:false,menuDisabled:true,sortable:false,tplGroup:''+'
    ux-action-right '+'{cls}" style="{style}" qtip="{qtip}">{text}
    '+'
    ',tplRow:'
    '+''+'
    '+'ux-row-action-text" style="{hide}{style}" qtip="{qtip}">'+'{text}
    '+'
    '+'
    ',hideMode:'visibility',widthIntercept:4,widthSlope:21,init:function(g){this.grid=g;this.id=this.id||Ext.id();var h=g.getColumnModel().lookup;delete(h[undefined]);h[this.id]=this;if(!this.tpl){this.tpl=this.processActions(this.actions)}if(this.autoWidth){this.width=this.widthSlope*this.actions.length+this.widthIntercept;this.fixed=true}var i=g.getView();var j={scope:this};j[this.actionEvent]=this.onClick;g.afterRender=g.afterRender.createSequence(function(){i.mainBody.on(j);g.on('destroy',this.purgeListeners,this)},this);if(!this.renderer){this.renderer=function(a,b,c,d,e,f){b.css+=(b.css?' ':'')+'ux-row-action-cell';return this.tpl.apply(this.getData(a,b,c,d,e,f))}.createDelegate(this)}if(i.groupTextTpl&&this.groupActions){i.interceptMouse=i.interceptMouse.createInterceptor(function(e){if(e.getTarget('.ux-grow-action-item')){return false}});i.groupTextTpl='
    '+i.groupTextTpl+'
    '+this.processActions(this.groupActions,this.tplGroup).apply()}if(true===this.keepSelection){g.processEvent=g.processEvent.createInterceptor(function(a,e){if('mousedown'===a){return!this.getAction(e)}},this)}},getData:function(a,b,c,d,e,f){return c.data||{}},processActions:function(b,c){var d=[];Ext.each(b,function(a,i){if(a.iconCls&&'function'===typeof(a.callback||a.cb)){this.callbacks=this.callbacks||{};this.callbacks[a.iconCls]=a.callback||a.cb}var o={cls:a.iconIndex?'{'+a.iconIndex+'}':(a.iconCls?a.iconCls:''),qtip:a.qtipIndex?'{'+a.qtipIndex+'}':(a.tooltip||a.qtip?a.tooltip||a.qtip:''),text:a.textIndex?'{'+a.textIndex+'}':(a.text?a.text:''),hide:a.hideIndex?''+('display'===this.hideMode?'display:none':'visibility:hidden')+';':(a.hide?('display'===this.hideMode?'display:none':'visibility:hidden;'):''),align:a.align||'right',style:a.style?a.style:''};d.push(o)},this);var e=new Ext.XTemplate(c||this.tplRow);return new Ext.XTemplate(e.apply({actions:d}))},getAction:function(e){var a=false;var t=e.getTarget('.ux-row-action-item');if(t){a=t.className.replace(/ux-row-action-item /,'');if(a){a=a.replace(/ ux-row-action-text/,'');a=a.trim()}}return a},onClick:function(e,a){var b=this.grid.getView();var c=e.getTarget('.x-grid3-row');var d=b.findCellIndex(a.parentNode.parentNode);var f=this.getAction(e);if(false!==c&&false!==d&&false!==f){var g=this.grid.store.getAt(c.rowIndex);if(this.callbacks&&'function'===typeof this.callbacks[f]){this.callbacks[f](this.grid,g,f,c.rowIndex,d)}if(true!==this.eventsSuspended&&false===this.fireEvent('beforeaction',this.grid,g,f,c.rowIndex,d)){return}else if(true!==this.eventsSuspended){this.fireEvent('action',this.grid,g,f,c.rowIndex,d)}}t=e.getTarget('.ux-grow-action-item');if(t){var h=b.findGroup(a);var i=h?h.id.replace(/ext-gen[0-9]+-gp-/,''):null;var j;if(i){var k=new RegExp(RegExp.escape(i));j=this.grid.store.queryBy(function(r){return r._groupId.match(k)});j=j?j.items:[]}f=t.className.replace(/ux-grow-action-item (ux-action-right )*/,'');if('function'===typeof this.callbacks[f]){this.callbacks[f](this.grid,j,f,i)}if(true!==this.eventsSuspended&&false===this.fireEvent('beforegroupaction',this.grid,j,f,i)){return false}this.fireEvent('groupaction',this.grid,j,f,i)}}});Ext.reg('rowactions',Ext.ux.grid.RowActions); +Ext.ns('Ext.ux.grid'); +if (typeof RegExp.escape !== 'function') { + RegExp.escape = function(s) { + if (typeof s !== 'string') { + return s; + } + return s.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, '\\$1'); + }; +} +Ext.ux.grid.RowActions = function(a) { + Ext.apply(this, a); + this.addEvents('beforeaction', 'action', 'beforegroupaction', 'groupaction'); + Ext.ux.grid.RowActions.superclass.constructor.call(this); +}; +Ext.extend(Ext.ux.grid.RowActions, Ext.util.Observable, { + actionEvent: 'click', + autoWidth: true, + dataIndex: '', + editable: false, + header: '', + isColumn: true, + keepSelection: false, + menuDisabled: true, + sortable: false, + tplGroup: '
    ux-action-right {cls}" style="{style}" qtip="{qtip}">{text}
    ', + tplRow: '
    ux-row-action-text" style="{hide}{style}" qtip="{qtip}">{text}
    ', + hideMode: 'visibility', + widthIntercept: 4, + widthSlope: 21, + init: function(g) { + this.grid = g; + this.id = this.id || Ext.id(); + const h = g.getColumnModel().lookup; + delete h[undefined]; + h[this.id] = this; + if (!this.tpl) { + this.tpl = this.processActions(this.actions); + } + if (this.autoWidth) { + this.width = this.widthSlope * this.actions.length + this.widthIntercept; + this.fixed = true; + } + const + i = g.getView(), + j = { scope: this } + ; + j[this.actionEvent] = this.onClick; + g.afterRender = g.afterRender.createSequence(function() { + i.mainBody.on(j); + g.on('destroy', this.purgeListeners, this); + }, this); + if (!this.renderer) { + this.renderer = function(a, b, c, d, e, f) { + b.css += `${b.css ? ' ' : ''}ux-row-action-cell`; + return this.tpl.apply(this.getData(a, b, c, d, e, f)); + }.createDelegate(this); + } + if (i.groupTextTpl && this.groupActions) { + i.interceptMouse = i.interceptMouse.createInterceptor(function(e) { + if (e.getTarget('.ux-grow-action-item')) { + return false; + } + }); + i.groupTextTpl = `
    ${i.groupTextTpl}
    ${this.processActions(this.groupActions, this.tplGroup).apply()}`; + } + if (this.keepSelection === true) { + g.processEvent = g.processEvent.createInterceptor(function(a, e) { + if (a === 'mousedown') { + return !this.getAction(e); + } + }, this); + } + }, + getData: function(a, b, c, d, e, f) { + return c.data || {}; + }, + processActions: function(b, c) { + const d = []; + Ext.each( + b, + function(a, i) { + if (a.iconCls && typeof (a.callback || a.cb) === 'function') { + this.callbacks = this.callbacks || {}; + this.callbacks[a.iconCls] = a.callback || a.cb; + } + const o = { + /* eslint-disable no-nested-ternary */ + cls: a.iconIndex ? `{${a.iconIndex}}` : a.iconCls ? a.iconCls : '', + qtip: a.qtipIndex ? `{${a.qtipIndex}}` : a.tooltip || a.qtip ? a.tooltip || a.qtip : '', + text: a.textIndex ? `{${a.textIndex}}` : a.text ? a.text : '', + hide: a.hideIndex + ? `${this.hideMode === 'display' ? 'display:none' : 'visibility:hidden'};` + : a.hide ? (this.hideMode === 'display' ? 'display:none' : 'visibility:hidden;') : '', + align: a.align || 'right', + style: a.style ? a.style : '' + }; + /* eslint-enable no-nested-ternary */ + d.push(o); + }, + this + ); + const e = new Ext.XTemplate(c || this.tplRow); + return new Ext.XTemplate(e.apply({ actions: d })); + }, + getAction: function(e) { + let a = false; + const t = e.getTarget('.ux-row-action-item'); + if (t) { + a = t.className.replace(/ux-row-action-item /, ''); + if (a) { + a = a.replace(/ ux-row-action-text/, ''); + a = a.trim(); + } + } + return a; + }, + onClick: function(e, a) { + const + b = this.grid.getView(), + c = e.getTarget('.x-grid3-row'), + d = b.findCellIndex(a.parentNode.parentNode) + ; + let f = this.getAction(e); + if (c !== false && d !== false && f !== false) { + const g = this.grid.store.getAt(c.rowIndex); + if (this.callbacks && typeof this.callbacks[f] === 'function') { + this.callbacks[f](this.grid, g, f, c.rowIndex, d); + } + if (this.eventsSuspended !== true && this.fireEvent('beforeaction', this.grid, g, f, c.rowIndex, d) === false) { + return; + } + if (this.eventsSuspended !== true) { + this.fireEvent('action', this.grid, g, f, c.rowIndex, d); + } + } + const t = e.getTarget('.ux-grow-action-item'); + if (t) { + const + h = b.findGroup(a), + i = h ? h.id.replace(/ext-gen[0-9]+-gp-/, '') : null + ; + let j; + if (i) { + const k = new RegExp(RegExp.escape(i)); + j = this.grid.store.queryBy(function(r) { + return r._groupId.match(k); + }); + j = j ? j.items : []; + } + f = t.className.replace(/ux-grow-action-item (ux-action-right )*/, ''); + if (typeof this.callbacks[f] === 'function') { + this.callbacks[f](this.grid, j, f, i); + } + if (this.eventsSuspended !== true && this.fireEvent('beforegroupaction', this.grid, j, f, i) === false) { + return false; + } + this.fireEvent('groupaction', this.grid, j, f, i); + } + } +}); +Ext.reg('rowactions', Ext.ux.grid.RowActions); -/* +/** * Ext JS Library 0.30 * Copyright(c) 2006-2009, Ext JS, LLC. * licensing@extjs.com * - * http://extjs.com/license + * @deprecated No use found in the core as of 3.x; remove? */ Ext.SwitchButton = Ext.extend(Ext.Component, { - initComponent : function(){ + initComponent: function() { Ext.SwitchButton.superclass.initComponent.call(this); - var mc = new Ext.util.MixedCollection(); + const mc = new Ext.util.MixedCollection(); mc.addAll(this.items); this.items = mc; this.addEvents('change'); - if(this.handler){ + if (this.handler) { this.on('change', this.handler, this.scope || this); } }, - onRender : function(ct, position){ - var el = document.createElement('table'); + onRender: function(ct, position) { + const el = document.createElement('table'); el.cellSpacing = 0; el.className = 'x-rbtn'; el.id = this.id; - var row = document.createElement('tr'); + const row = document.createElement('tr'); el.appendChild(document.createElement('tbody')).appendChild(row); - var count = this.items.length; - var last = count - 1; + const + count = this.items.length, + last = count - 1 + ; this.activeItem = this.items.get(this.activeItem); - for(var i = 0; i < count; i++){ - var item = this.items.itemAt(i); - - var cell = row.appendChild(document.createElement('td')); - cell.id = this.id + '-rbi-' + i; + for (let i = 0; i < count; i++) { + const + item = this.items.itemAt(i), + cell = row.appendChild(document.createElement('td')), + nextCls = i === last ? 'x-rbtn-last' : 'x-rbtn-item' + ; + cell.id = `${this.id}-rbi-${i}`; - var cls = i == 0 ? 'x-rbtn-first' : (i == last ? 'x-rbtn-last' : 'x-rbtn-item'); + let cls = i === 0 ? 'x-rbtn-first' : nextCls; item.baseCls = cls; - if(this.activeItem == item){ + if (this.activeItem === item) { cls += '-active'; } cell.className = cls; - var button = document.createElement('button'); + const button = document.createElement('button'); button.innerHTML = ' '; button.className = item.iconCls; button.qtip = item.tooltip; @@ -816,21 +1021,21 @@ Ext.SwitchButton = Ext.extend(Ext.Component, { this.el.on('click', this.onClick, this); }, - getActiveItem : function(){ + getActiveItem: function() { return this.activeItem; }, - setActiveItem : function(item){ - if(typeof item != 'object' && item !== null){ + setActiveItem: function(item) { + if (typeof item != 'object' && item !== null) { item = this.items.get(item); } - var current = this.getActiveItem(); - if(item != current){ - if(current){ - Ext.fly(current.cell).removeClass(current.baseCls + '-active'); + const current = this.getActiveItem(); + if (item !== current) { + if (current) { + Ext.fly(current.cell).removeClass(`${current.baseCls}-active`); } - if(item) { - Ext.fly(item.cell).addClass(item.baseCls + '-active'); + if (item) { + Ext.fly(item.cell).addClass(`${item.baseCls}-active`); } this.activeItem = item; this.fireEvent('change', this, item); @@ -838,14 +1043,13 @@ Ext.SwitchButton = Ext.extend(Ext.Component, { return item; }, - onClick : function(e){ - var target = e.getTarget('td', 2); - if(!this.disabled && target){ + onClick: function(e) { + const target = e.getTarget('td', 2); + if (!this.disabled && target) { this.setActiveItem(parseInt(target.id.split('-rbi-')[1], 10)); } } }); - Ext.reg('switch', Ext.SwitchButton); Ext.onReady(function() { @@ -853,12 +1057,217 @@ Ext.onReady(function() { MODx.form.Handler = MODx.load({ xtype: 'modx-form-handler' }); MODx.msg = MODx.load({ xtype: 'modx-msg' }); }); + /* always-submit checkboxes */ -Ext.form.XCheckbox=Ext.extend(Ext.form.Checkbox,{submitOffValue:0,submitOnValue:1,onRender:function(){this.inputValue=this.submitOnValue;Ext.form.XCheckbox.superclass.onRender.apply(this,arguments);this.hiddenField=this.wrap.insertFirst({tag:'input',type:'hidden'});if(this.tooltip){this.imageEl.set({qtip:this.tooltip})}this.updateHidden()},setValue:function(v){v=this.convertValue(v);this.updateHidden(v);Ext.form.XCheckbox.superclass.setValue.apply(this,arguments)},updateHidden:function(v){v=undefined!==v?v:this.checked;v=this.convertValue(v);if(this.hiddenField){this.hiddenField.dom.value=v?this.submitOnValue:this.submitOffValue;this.hiddenField.dom.name=v?'':this.el.dom.name}},convertValue:function(v){return(v===true||v==='true'||v===this.submitOnValue||String(v).toLowerCase()==='on')}});Ext.reg('xcheckbox',Ext.form.XCheckbox); +Ext.form.XCheckbox = Ext.extend(Ext.form.Checkbox, { + submitOffValue: 0, + submitOnValue: 1, + onRender: function() { + this.inputValue = this.submitOnValue; + Ext.form.XCheckbox.superclass.onRender.apply(this, arguments); + this.hiddenField = this.wrap.insertFirst({ + tag: 'input', + type: 'hidden' + }); + if (this.tooltip) { + this.imageEl.set({ qtip: this.tooltip }); + } + this.updateHidden(); + }, + setValue: function(v) { + v = this.convertValue(v); + this.updateHidden(v); + Ext.form.XCheckbox.superclass.setValue.apply(this, arguments); + }, + updateHidden: function(v) { + v = undefined !== v ? v : this.checked; + v = this.convertValue(v); + if (this.hiddenField) { + this.hiddenField.dom.value = v ? this.submitOnValue : this.submitOffValue; + this.hiddenField.dom.name = v ? '' : this.el.dom.name; + } + }, + convertValue: function(v) { + return v === true || v === 'true' || v === this.submitOnValue || String(v).toLowerCase() === 'on'; + } +}); +Ext.reg('xcheckbox', Ext.form.XCheckbox); /* drag/drop grids */ -Ext.namespace('Ext.ux.dd');Ext.ux.dd.GridDragDropRowOrder=Ext.extend(Ext.util.Observable,{copy:false,scrollable:false,constructor:function(config){if(config)Ext.apply(this,config);this.addEvents({beforerowmove:true,afterrowmove:true,beforerowcopy:true,afterrowcopy:true});Ext.ux.dd.GridDragDropRowOrder.superclass.constructor.call(this)},init:function(grid){this.grid=grid;grid.enableDragDrop=true;grid.on({render:{fn:this.onGridRender,scope:this,single:true}})},onGridRender:function(grid){var self=this;this.target=new Ext.dd.DropTarget(grid.getEl(),{ddGroup:grid.ddGroup||'GridDD',grid:grid,gridDropTarget:this,notifyDrop:function(dd,e,data){if(this.currentRowEl){this.currentRowEl.removeClass('grid-row-insert-below');this.currentRowEl.removeClass('grid-row-insert-above')}var t=Ext.lib.Event.getTarget(e);var rindex=this.grid.getView().findRowIndex(t);if(rindex===false||rindex==data.rowIndex){return false}if(this.gridDropTarget.fireEvent(self.copy?'beforerowcopy':'beforerowmove',this.gridDropTarget,data.rowIndex,rindex,data.selections,123)===false){return false}var ds=this.grid.getStore();var selections=new Array();var keys=ds.data.keys;for(var key in keys){for(var i=0;idata.rowIndex&&this.rowPosition<0){rindex--}if(rindex0){rindex++}if(rindex>data.rowIndex&&data.selections.length>1){rindex=rindex-(data.selections.length-1)}if(rindex==data.rowIndex){return false}if(!self.copy){for(var i=0;i=0;i--){var insertIndex=rindex;ds.insert(insertIndex,selections[i])}var sm=this.grid.getSelectionModel();if(sm){sm.selectRecords(data.selections)}this.gridDropTarget.fireEvent(self.copy?'afterrowcopy':'afterrowmove',this.gridDropTarget,data.rowIndex,rindex,data.selections);return true},notifyOver:function(dd,e,data){var t=Ext.lib.Event.getTarget(e);var rindex=this.grid.getView().findRowIndex(t);var ds=this.grid.getStore();var keys=ds.data.keys;for(var key in keys){for(var i=0;i0){this.currentRowEl=new Ext.Element(currentRow);this.currentRowEl.addClass('grid-row-insert-below')}else{if(rindex-1>=0){var previousRow=this.grid.getView().getRow(rindex-1);this.currentRowEl=new Ext.Element(previousRow);this.currentRowEl.addClass('grid-row-insert-below')}else{this.currentRowEl.addClass('grid-row-insert-above')}}}catch(err){console.warn(err);rindex=false}return(rindex===false)?this.dropNotAllowed:this.dropAllowed},notifyOut:function(dd,e,data){if(this.currentRowEl){this.currentRowEl.removeClass('grid-row-insert-above');this.currentRowEl.removeClass('grid-row-insert-below')}}});if(this.targetCfg){Ext.apply(this.target,this.targetCfg)}if(this.scrollable){Ext.dd.ScrollManager.register(grid.getView().getEditorParent());grid.on({beforedestroy:this.onBeforeDestroy,scope:this,single:true})}},getTarget:function(){return this.target},getGrid:function(){return this.grid},getCopy:function(){return this.copy?true:false},setCopy:function(b){this.copy=b?true:false},onBeforeDestroy:function(grid){Ext.dd.ScrollManager.unregister(grid.getView().getEditorParent())}}); +Ext.namespace('Ext.ux.dd'); +Ext.ux.dd.GridDragDropRowOrder = Ext.extend(Ext.util.Observable, { + copy: false, + scrollable: false, + constructor: function(config) { + if (config) { + Ext.apply(this, config); + } + this.addEvents({ + beforerowmove: true, + afterrowmove: true, + beforerowcopy: true, + afterrowcopy: true + }); + Ext.ux.dd.GridDragDropRowOrder.superclass.constructor.call(this); + }, + init: function(grid) { + this.grid = grid; + grid.enableDragDrop = true; + grid.on({ + render: { + fn: this.onGridRender, + scope: this, + single: true + } + }); + }, + onGridRender: function(grid) { + const self = this; + this.target = new Ext.dd.DropTarget(grid.getEl(), { + ddGroup: grid.ddGroup || 'GridDD', + grid: grid, + gridDropTarget: this, + notifyDrop: function(dd, e, data) { + if (this.currentRowEl) { + this.currentRowEl.removeClass('grid-row-insert-below'); + this.currentRowEl.removeClass('grid-row-insert-above'); + } + const target = Ext.lib.Event.getTarget(e); + let rindex = this.grid.getView().findRowIndex(target); + if (rindex === false || rindex === data.rowIndex) { + return false; + } + if (this.gridDropTarget.fireEvent(self.copy ? 'beforerowcopy' : 'beforerowmove', this.gridDropTarget, data.rowIndex, rindex, data.selections, 123) === false) { + return false; + } + const + ds = this.grid.getStore(), + selections = [], + { keys } = ds.data + ; + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const key in keys) { + for (let i = 0; i < data.selections.length; i++) { + if (keys[key] === data.selections[i].id) { + if (rindex === key) { + return false; + } + selections.push(data.selections[i]); + } + } + } + if (rindex > data.rowIndex && this.rowPosition < 0) { + rindex--; + } + if (rindex < data.rowIndex && this.rowPosition > 0) { + rindex++; + } + if (rindex > data.rowIndex && data.selections.length > 1) { + rindex -= (data.selections.length - 1); + } + if (rindex === data.rowIndex) { + return false; + } + if (!self.copy) { + for (let i = 0; i < data.selections.length; i++) { + ds.remove(ds.getById(data.selections[i].id)); + } + } + for (let i = selections.length - 1; i >= 0; i--) { + const insertIndex = rindex; + ds.insert(insertIndex, selections[i]); + } + const sm = this.grid.getSelectionModel(); + if (sm) { + sm.selectRecords(data.selections); + } + this.gridDropTarget.fireEvent(self.copy ? 'afterrowcopy' : 'afterrowmove', this.gridDropTarget, data.rowIndex, rindex, data.selections); + return true; + }, + notifyOver: function(dd, e, data) { + const + target = Ext.lib.Event.getTarget(e), + ds = this.grid.getStore(), + { keys } = ds.data + ; + let rindex = this.grid.getView().findRowIndex(target); + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const key in keys) { + for (let i = 0; i < data.selections.length; i++) { + if (keys[key] === data.selections[i].id) { + if (rindex === key) { + if (this.currentRowEl) { + this.currentRowEl.removeClass('grid-row-insert-below'); + this.currentRowEl.removeClass('grid-row-insert-above'); + } + return this.dropNotAllowed; + } + } + } + } + if (rindex < 0 || rindex === false) { + this.currentRowEl.removeClass('grid-row-insert-above'); + return this.dropNotAllowed; + } + try { + const + currentRow = this.grid.getView().getRow(rindex), + resolvedRow = new Ext.Element(currentRow).getY() - this.grid.getView().scroller.dom.scrollTop, + rowHeight = currentRow.offsetHeight + ; + this.rowPosition = e.getPageY() - resolvedRow - rowHeight / 2; + if (this.currentRowEl) { + this.currentRowEl.removeClass('grid-row-insert-below'); + this.currentRowEl.removeClass('grid-row-insert-above'); + } + if (this.rowPosition > 0) { + this.currentRowEl = new Ext.Element(currentRow); + this.currentRowEl.addClass('grid-row-insert-below'); + } else if (rindex - 1 >= 0) { + const previousRow = this.grid.getView().getRow(rindex - 1); + this.currentRowEl = new Ext.Element(previousRow); + this.currentRowEl.addClass('grid-row-insert-below'); + } else { + this.currentRowEl.addClass('grid-row-insert-above'); + } + } catch (err) { + console.warn(err); + rindex = false; + } + return rindex === false ? this.dropNotAllowed : this.dropAllowed; + }, + notifyOut: function(dd, e, data) { + if (this.currentRowEl) { + this.currentRowEl.removeClass('grid-row-insert-above'); + this.currentRowEl.removeClass('grid-row-insert-below'); + } + } + }); + if (this.targetCfg) { + Ext.apply(this.target, this.targetCfg); + } + if (this.scrollable) { + Ext.dd.ScrollManager.register(grid.getView().getEditorParent()); + grid.on({ beforedestroy: this.onBeforeDestroy, scope: this, single: true }); + } + }, + getTarget: function() { + return this.target; + }, + getGrid: function() { + return this.grid; + }, + getCopy: function() { + return this.copy; + }, + setCopy: function(copy) { + this.copy = copy; + }, + onBeforeDestroy: function(grid) { + Ext.dd.ScrollManager.unregister(grid.getView().getEditorParent()); + } +}); /** selectability in Ext grids */ if (!Ext.grid.GridView.prototype.templates) { @@ -872,20 +1281,22 @@ Ext.grid.GridView.prototype.templates.cell = new Ext.Template( /* combocolumn */ if (!MODx.grid) { MODx.grid = {}; } -MODx.grid.ComboColumn = Ext.extend(Ext.grid.Column,{ - gridId: undefined - ,constructor: function(cfg){ +MODx.grid.ComboColumn = Ext.extend(Ext.grid.Column, { + gridId: undefined, + constructor: function(cfg) { MODx.grid.ComboColumn.superclass.constructor.call(this, cfg); - this.renderer = (this.editor && this.editor.triggerAction) ? MODx.grid.ComboBoxRenderer(this.editor,this.gridId, cfg.renderer) : function(value) {return value;}; + this.renderer = (this.editor && this.editor.triggerAction) ? MODx.grid.ComboBoxRenderer(this.editor, this.gridId, cfg.renderer) : function(value) { return value; }; } }); -Ext.grid.Column.types['combocolumn'] = MODx.grid.ComboColumn; +Ext.grid.Column.types.combocolumn = MODx.grid.ComboColumn; MODx.grid.ComboBoxRenderer = function(combo, gridId, currentRenderer) { - var getValue = function(value) { - var idx = combo.store.find(combo.valueField, value); - var rec = combo.store.getAt(idx); - if (rec) { - return rec.get(combo.displayField); + const getValue = value => { + const + idx = combo.store.find(combo.valueField, value), + record = combo.store.getAt(idx) + ; + if (record) { + return record.get(combo.displayField); } return value; }; @@ -893,7 +1304,7 @@ MODx.grid.ComboBoxRenderer = function(combo, gridId, currentRenderer) { return function(value, metaData, record, rowIndex, colIndex, store) { if (currentRenderer) { if (typeof currentRenderer.fn === 'function') { - var scope = (currentRenderer.scope) ? currentRenderer.scope : false; + const scope = (currentRenderer.scope) ? currentRenderer.scope : false; currentRenderer = currentRenderer.fn.bind(scope); } @@ -902,23 +1313,19 @@ MODx.grid.ComboBoxRenderer = function(combo, gridId, currentRenderer) { } } - if (combo.store.getCount() == 0 && gridId) { - combo.store.on( - 'load', - function() { - var grid = Ext.getCmp(gridId); - if (grid) { - grid.getView().refresh(); - } - }, this, {single: true} - ); + if (combo.store.getCount() === 0 && gridId) { + combo.store.on('load', function() { + const grid = Ext.getCmp(gridId); + if (grid) { + grid.getView().refresh(); + } + }, this, { single: true }); return value; } return getValue(value); }; }; - Ext.Button.buttonTemplate = new Ext.Template( '' ); @@ -931,7 +1338,6 @@ Ext.TabPanel.prototype.itemTpl = new Ext.Template( Ext.TabPanel.prototype.itemTpl.disableFormats = true; Ext.TabPanel.prototype.itemTpl.compile(); - Ext.namespace('Ext.ux.form'); /** @@ -943,8 +1349,9 @@ Ext.ux.form.CheckboxGroup = Ext.extend(Ext.form.CheckboxGroup, { aggregateSubmitField: {}, initComponent: function() { - const me = this, - ct = this.ownerCt + const + me = this, + ct = this.ownerCt ; if (typeof this.name === 'string' && this.name.length > 0) { this.aggregateSubmitField = new Ext.form.Hidden({ @@ -953,12 +1360,13 @@ Ext.ux.form.CheckboxGroup = Ext.extend(Ext.form.CheckboxGroup, { Ext.ux.form.CheckboxGroup.superclass.initComponent.call(this); - this.cls = typeof this.cls === 'string' && this.cls.length > 0 ? 'aggregated-group ' + this.cls : 'aggregated-group' ; + this.cls = typeof this.cls === 'string' && this.cls.length > 0 ? `aggregated-group ${this.cls}` : 'aggregated-group' ; + // eslint-disable-next-line func-names, prefer-arrow-callback Ext.each(this.items, function(item) { if (typeof me.value === 'string' && me.value.length > 0) { const savedVals = me.value.split(','); - if (savedVals.find(function(v){ return v == item.inputValue; }) == item.inputValue) { + if (savedVals.find(function(v) { return v === item.inputValue; }) === item.inputValue) { item.checked = true; } me.aggregateSubmitField.setValue(me.value); @@ -968,7 +1376,7 @@ Ext.ux.form.CheckboxGroup = Ext.extend(Ext.form.CheckboxGroup, { fn: me.setHiddenSubmit, scope: me } - } + }; item.submitValue = false; }); ct.add(this.aggregateSubmitField); @@ -977,8 +1385,10 @@ Ext.ux.form.CheckboxGroup = Ext.extend(Ext.form.CheckboxGroup, { } }, setHiddenSubmit: function() { - const groupOpts = this.items.items; - let vals = []; + const + groupOpts = this.items.items, + vals = [] + ; Ext.each(groupOpts, function(item) { if (item.checked) { vals.push(item.inputValue); @@ -998,7 +1408,7 @@ Ext.reg('xcheckboxgroup', Ext.ux.form.CheckboxGroup); */ Ext.define('AddFieldUtilities.plugin.Class', { alias: 'plugin.fieldutilities', - init: function(cmp){ + init: function(cmp) { cmp.on('afterrender', this.afterRender, cmp); }, afterRender: function() { @@ -1009,7 +1419,7 @@ Ext.define('AddFieldUtilities.plugin.Class', { tag: 'a', title: _('field_reset'), cls: 'modx-field-utils modx-field-reset' - }).on('click', function(){ + }).on('click', function() { me.reset(); }, me); @@ -1018,8 +1428,8 @@ Ext.define('AddFieldUtilities.plugin.Class', { tag: 'a', title: _('field_clear'), cls: 'modx-field-utils modx-field-clear' - }).on('click', function(){ - switch(this.xtype) { + }).on('click', function() { + switch (this.xtype) { case 'xcheckboxgroup': case 'checkboxgroup': if (Ext.isArray(this.items.items)) { @@ -1060,43 +1470,71 @@ Ext.define('AddFieldUtilities.plugin.Class', { // setcookie($this->getProperty('cookieName'), 'true', time() + 10, '/'); // } -MODx.util.FileDownload = function (fields) { +MODx.util.FileDownload = function(fields) { if (!Ext.isObject(fields)) { return; } + let polling = fields.timeout * 10 || 300; + const + me = this, + cookieName = `fileDownload${me.randomHex(16)}`, + ident = fields.ident || `filedownload-${Ext.id()}`, + url = fields.url || MODx.config.connector_url, + params = fields.params || {}, + debug = fields.debug || false, + successCallback = fields.success || null, + failureCallback = fields.failure || null, + body = Ext.getBody(), + frame = body.createChild({ + tag: 'iframe', + cls: 'x-hidden', + id: `${ident}-iframe`, + name: `${ident}-iframe` + }), + form = body.createChild({ + tag: 'form', + cls: 'x-hidden', + id: `${ident}-form`, + action: url, + target: `${ident}-iframe`, + method: 'post' + }) + ; - var me = this; - me.clearCookie = function () { - Ext.util.Cookies.set(cookieName, null, new Date("January 1, 1970"), '/'); + me.clearCookie = function() { + Ext.util.Cookies.set(cookieName, null, new Date('January 1, 1970'), '/'); Ext.util.Cookies.clear(cookieName, '/'); - } - me.randomHex = function (len) { + }; + me.randomHex = function(len) { const hex = '0123456789ABCDEF'; let output = ''; for (let i = 0; i < len; ++i) { output += hex.charAt(Math.floor(Math.random() * hex.length)); } return output; - } - me.isFinished = function (successCallback, failureCallback) { + }; + me.isFinished = function(successCallback, failureCallback) { // Check if file is started downloading if (Ext.util.Cookies.get(cookieName) && Ext.util.Cookies.get(cookieName) === 'true') { me.clearCookie(); if (successCallback) { - successCallback({success: true, message: _('$file_msg_download_success')}); + successCallback({ + success: true, + message: _('$file_msg_download_success') + }); } return; } // Check for error / IF any error happens the frame will have content try { if (frame.dom.contentDocument.body.innerHTML.length > 0) { - var result = Ext.decode(frame.dom.contentDocument.body.innerHTML); - result = (result) ? result : {success: false, message: _('file_msg_download_error')}; + let result = Ext.decode(frame.dom.contentDocument.body.innerHTML); + result = result || { success: false, message: _('file_msg_download_error') }; me.clearCookie(); if (failureCallback) { failureCallback(result); } - frame.dom.contentDocument.body.innerHTML = ""; + frame.dom.contentDocument.body.innerHTML = ''; return; } } catch (e) { @@ -1105,10 +1543,10 @@ MODx.util.FileDownload = function (fields) { if (polling) { if (debug) { - console.log('polling ' + polling); + console.log(`polling ${polling}`); } // Download is not finished. Check again in 100 milliseconds. - window.setTimeout(function () { + window.setTimeout(function() { polling--; me.isFinished(successCallback, failureCallback); }, 100); @@ -1116,45 +1554,22 @@ MODx.util.FileDownload = function (fields) { // Polling timeout with no fileDownload cookie set me.clearCookie(); if (failureCallback) { - failureCallback({success: false, message: _('file_err_download_timeout')}); + failureCallback({ success: false, message: _('file_err_download_timeout') }); } } }; - var cookieName = 'fileDownload' + me.randomHex(16); - var polling = fields.timeout * 10 || 300; - var ident = fields.ident || 'filedownload-' + Ext.id(); - var url = fields.url || MODx.config.connector_url; - var params = fields.params || {}; - var debug = fields.debug || false; - var successCallback = fields.success || null; - var failureCallback = fields.failure || null; - - var body = Ext.getBody(); - var frame = body.createChild({ - tag: 'iframe', - cls: 'x-hidden', - id: ident + '-iframe', - name: ident + '-iframe', - }); - var form = body.createChild({ - tag: 'form', - cls: 'x-hidden', - id: ident + '-form', - action: url, - target: ident + '-iframe', - method: 'post', - }); params.HTTP_MODAUTH = MODx.siteId; if (typeof successCallback === 'function') { params.cookieName = cookieName; } - Ext.iterate(params, function (name, value) { + // eslint-disable-next-line func-names, prefer-arrow-callback + Ext.iterate(params, function(name, value) { form.createChild({ tag: 'input', type: 'text', cls: 'x-hidden', - id: ident + '-' + name, + id: `${ident}-${name}`, name: name, value: value }); From d81867466d8501ffb847fff7e3df6c4befe2d2d3 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 31 Oct 2024 17:40:21 -0400 Subject: [PATCH 02/39] Base updates Changes to js, css, and Lexicon common to multiple areas of this PR --- _build/templates/default/sass/_forms.scss | 109 +- _build/templates/default/sass/_utility.scss | 11 +- _build/templates/default/sass/index.scss | 58 +- core/lexicon/en/default.inc.php | 4 + manager/assets/modext/util/utilities.js | 14 + .../assets/modext/widgets/core/modx.grid.js | 2080 +++++++++++------ 6 files changed, 1488 insertions(+), 788 deletions(-) diff --git a/_build/templates/default/sass/_forms.scss b/_build/templates/default/sass/_forms.scss index 77c9e97b32..af942847ac 100644 --- a/_build/templates/default/sass/_forms.scss +++ b/_build/templates/default/sass/_forms.scss @@ -76,7 +76,7 @@ textarea.x-form-field, border-radius: $borderRadius; border: 1px solid $borderColor; position: relative; - transition: border-color .25s; + transition: border-color 0.25s; } .x-viewport .x-trigger-wrap-focus, @@ -142,7 +142,7 @@ input::-moz-focus-inner { padding: 0 0 0 3px; top: 0; right: 0; - transition: all .25s; + transition: all 0.25s; width: 16px; height: 16px; @@ -288,7 +288,7 @@ input::-moz-focus-inner { border: 1px solid $borderColor; border-radius: $borderRadius; padding: 5px; - transition: all .25s; + transition: all 0.25s; &:focus { border: 1px solid $borderColorFocus; @@ -409,7 +409,7 @@ input::-moz-focus-inner { } &.toggle-slider-above { - margin: .3em 0; + margin: 0.3em 0; padding-left: 3.9em; } @@ -421,15 +421,15 @@ input::-moz-focus-inner { .example-list { ul { - margin: .4em 0; + margin: 0.4em 0; li { position: relative; - margin-bottom: .25em; + margin-bottom: 0.25em; padding-left: 1.25em; &::before { @extend %pseudo-font; position: absolute; - left: .2em; + left: 0.2em; top: 0; content: fa-content($fa-var-angle-double-right); color: scale-color($mediumGray, $lightness: 20%); @@ -439,7 +439,7 @@ input::-moz-focus-inner { } .example-input, .copy-this { - padding: 0 .3em; + padding: 0 0.3em; border-radius: 2px; transition: width 1s; } @@ -477,7 +477,14 @@ input::-moz-focus-inner { } } } - } + &:active { + color: $darkGray; + &::after { + color: $darkGray; + } + } + } + .feedback { margin-left: 1.4rem; color: scale-color($blue, $lightness: -35%); @@ -506,7 +513,7 @@ input::-moz-focus-inner { .fs-toggle { padding-top: 1em; margin-top: 2em; - margin-bottom: .5em; + margin-bottom: 0.5em; border-top: 1px dashed $borderColor; } @@ -635,7 +642,7 @@ input::-moz-focus-inner { transform: translate(-50%, -50%); text-align: center; width: 30px; - transition: opacity .25s; + transition: opacity 0.25s; } &.x-form-trigger-over, @@ -931,11 +938,11 @@ input::-moz-focus-inner { left: unset; } - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { position: relative; padding-left: 3.6em; - padding-top: .2em; + padding-top: 0.2em; margin-left: 0; cursor: pointer; box-sizing: border-box; @@ -945,7 +952,7 @@ input::-moz-focus-inner { &:after { content: ''; position: absolute; - transition: all .2s ease; + transition: all 0.2s ease; font-size: inherit; } @@ -962,7 +969,7 @@ input::-moz-focus-inner { &:after { left: 0.1em; top: 0.8em; - margin-top: -.65em; + margin-top: -0.65em; height: 1.3em; width: 1.3em; border-radius: 50%; @@ -972,9 +979,8 @@ input::-moz-focus-inner { } &:checked { - - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { &:after { left: 1.6em; top: 0.8em; @@ -988,9 +994,8 @@ input::-moz-focus-inner { } &.danger:checked { - - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { &:before { background-color: $red; border-color: $red; @@ -999,9 +1004,8 @@ input::-moz-focus-inner { } &.warning:checked { - - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { &:before { background-color: $orange; border-color: $orange; @@ -1088,13 +1092,13 @@ input::-moz-focus-inner { cursor: pointer; display: inline-block; /*font-size: 1px;*/ outline: 0; /* fix firefox dotted outlines */ - opacity: .6; + opacity: 0.6; filter: alpha(opacity=60); /* for IE <= 8 */ padding: 0; position: absolute; top: 0; right: 0; - transition: opacity .25s; + transition: opacity 0.25s; width: 16px; height: 100%; @@ -1225,21 +1229,22 @@ input::-moz-focus-inner { } .x-editor .x-form-check-wrap { - background-color: $white -} - -/* fix combo on grid editor bug */ -.x-grid-editor .x-form-field-wrap { - background: #f6f2f7 url($imgPath + 'modx-theme/form/combo-bck.png') repeat-x scroll 0 100%; + background-color: $white; } -.x-grid-editor .x-form-field-wrap input { +.x-grid-editor { + z-index: 9002 !important; + .x-form-field-wrap { + background: #f6f2f7 url($imgPath + "modx-theme/form/combo-bck.png") repeat-x + scroll 0 100%; + input { background-color: transparent !important; } - -.x-grid-editor .x-form-field-wrap img { + img { background-color: $white; - background-image: url($imgPath + 'modx-theme/form/trigger.png'); + background-image: url($imgPath + "modx-theme/form/trigger.png"); + } + } } .x-form-grow-sizer { @@ -1373,7 +1378,7 @@ input::-moz-focus-inner { .x-btn { padding: 1px; - transition: color .25s; + transition: color 0.25s; &.x-btn-over, &:hover, @@ -1388,7 +1393,7 @@ input::-moz-focus-inner { &.x-item-disabled { color: $buttonColor; - opacity: .4; + opacity: 0.4; } button:before { @@ -1432,7 +1437,11 @@ input::-moz-focus-inner { } /* the second text cell, "of X" */ - .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell { + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell { .xtb-text { display: inline-block; position: absolute; @@ -1444,7 +1453,15 @@ input::-moz-focus-inner { } /* the last regular button >>, yes, I know it's ugly but tell that Microsoft and say thanks for IE8 =) */ - .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell { + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell { .x-btn { margin-right: 0; } @@ -1453,13 +1470,13 @@ input::-moz-focus-inner { /* the refresh button */ .x-toolbar-cell:last-child { opacity: 0; - transition: opacity .25s; + transition: opacity 0.25s; .x-btn { font-size: 12px; line-height: 1; margin: 0; - opacity: .4; + opacity: 0.4; padding: 0; position: absolute; bottom: 2px; @@ -1548,11 +1565,11 @@ input::-moz-focus-inner { .x-date-mp-ybtn a.x-date-mp-prev, .x-date-mp-ybtn a.x-date-mp-next { display: inline-block; - opacity: .6; + opacity: 0.6; filter: alpha(opacity=60); /* for IE <= 8 */ margin: 0 auto; position: relative; - transition: opacity .25s; + transition: opacity 0.25s; &:before { @extend %pseudo-font; @@ -1809,6 +1826,6 @@ td.x-date-mp-sep { bottom: 0; padding: 10px 20px; color: #fff; - background-color: rgba(0, 0, 0, .8); + background-color: rgba(0, 0, 0, 0.8); } } diff --git a/_build/templates/default/sass/_utility.scss b/_build/templates/default/sass/_utility.scss index 27c6b20ed1..b5ff5bc8cd 100644 --- a/_build/templates/default/sass/_utility.scss +++ b/_build/templates/default/sass/_utility.scss @@ -145,7 +145,6 @@ } } - /* Instead of writing the same code for every nav bar */ @mixin navigation-list { list-style-type: none; @@ -212,3 +211,13 @@ } } } + +@mixin textLink { + color: $colorSplash; + text-decoration-style: dotted; + text-decoration-color: scale-color($colorSplash, $lightness: 50%); + &:hover { + color: $black; + border-bottom-color: scale-color($black, $lightness: 40%); + } +} \ No newline at end of file diff --git a/_build/templates/default/sass/index.scss b/_build/templates/default/sass/index.scss index 67a987c5d6..e27005359b 100644 --- a/_build/templates/default/sass/index.scss +++ b/_build/templates/default/sass/index.scss @@ -541,6 +541,14 @@ textarea.x-form-field { } /* grids */ + +.modx-protected-row { + .x-grid3-cell-inner { + font-style: italic; + color: $colorSplash; + } +} + .x-small-editor .x-form-field { font-size: 12px !important; } @@ -553,14 +561,11 @@ textarea.x-form-field { color: #999 !important; } -a.x-grid-link { - color: $colorSplash; - text-decoration: underline; -} - -a.x-grid-link:hover, -a.x-grid-link:focus { - text-decoration: none; +.x-grid-link { + @include textLink; + &.simulated-link { + cursor: pointer; + } } .x-editable-column { @@ -910,6 +915,37 @@ a.x-grid-link:focus { } /* rowactions */ +.x-grid3-row { + &.disable-selection, + &.disable-selection.x-grid3-row-selected { + .x-grid3-row-checker { + position: relative; + &::before, + &::after { + color: $disabledTextColor; + } + &::before { + content: '\f0c8'; + } + &::after { + content: '\f715'; + font-size: 6px; + position: absolute; + left: 50%; + top: 50%; + margin-left: 2px; + margin-top: 1px; + transform: translate(-50%, -50%) rotate(98deg); + font-weight: 600; + font-family: 'Font Awesome 5 Free'; + } + &:hover { + cursor: default; + } + } + } +} + .ux-row-action-cell .x-grid3-cell-inner { padding: 1px 0 0 0; } @@ -1883,6 +1919,12 @@ iframe[classname="x-hidden"] { user-select: text !important; } +.x-selectable { + &.simulated-link * { + @include textLink; + } +} + /* Lightbox */ #ux-lightbox { left: 0; diff --git a/core/lexicon/en/default.inc.php b/core/lexicon/en/default.inc.php index 58145332f7..8f2386dee4 100644 --- a/core/lexicon/en/default.inc.php +++ b/core/lexicon/en/default.inc.php @@ -75,6 +75,7 @@ $_lang['confirm_delete_message'] = 'Are you sure you want to delete this message?'; $_lang['confirm_remove'] = 'Are you sure you want to delete this item?'; $_lang['confirm_remove_locks'] = 'Users sometimes close their browser while editing documents, templates, snippets or parsers, possibly leaving the item they were editing in locked state. By pressing OK you can delete ALL locks currently in place.

    Proceed?'; +$_lang['confirm_remove_multiple'] = 'Are you sure you want to delete the selected items?'; $_lang['confirm_undelete'] = 'Any children documents deleted at the same time as this document will also be undeleted, but children documents deleted at an earlier time will still be deleted.'; $_lang['confirm_unpublish'] = 'Un-publishing this document now will delete any (un)publishing dates that may have been set. If you wish to set or keep publish or unpublish dates, please choose to edit the document instead.\n\nProceed?'; $_lang['console'] = 'Console'; @@ -92,6 +93,7 @@ $_lang['create_user_group'] = 'Create User Group'; $_lang['created'] = 'Created'; $_lang['createdon'] = 'Creation date'; +$_lang['creator'] = 'Creator'; $_lang['current'] = 'Current'; $_lang['dashboard'] = 'Dashboard'; $_lang['data_err_load'] = 'Error loading data.'; @@ -216,6 +218,8 @@ $_lang['general_information'] = 'General Information'; $_lang['general_settings'] = 'General Settings'; $_lang['go'] = 'Go'; +$_lang['grid_column_creator_header'] = $_lang['creator']; +$_lang['grid_column_creator_description'] = 'Indicates the entity that created the row’s data/setting (read-only)'; $_lang['group'] = 'Group'; $_lang['guid'] = 'GUID'; $_lang['handler'] = 'Handler'; diff --git a/manager/assets/modext/util/utilities.js b/manager/assets/modext/util/utilities.js index 555d2dbc2f..9d3bfa14ac 100644 --- a/manager/assets/modext/util/utilities.js +++ b/manager/assets/modext/util/utilities.js @@ -144,6 +144,20 @@ MODx.StaticTextField = Ext.extend(Ext.form.TextField, { }); Ext.reg('statictextfield', MODx.StaticTextField); +/** + * Static Textarea + */ +MODx.StaticTextArea = Ext.extend(Ext.form.TextArea, { + fieldClass: 'x-static-text-field', + + onRender: function() { + this.readOnly = true; + this.disabled = !this.initialConfig.submitValue; + MODx.StaticTextArea.superclass.onRender.apply(this, arguments); + } +}); +Ext.reg('statictextarea', MODx.StaticTextArea); + /** * Static Boolean */ diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index fb88431782..1c3aeacddc 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -5,98 +5,103 @@ MODx.grid.Grid = function(config = {}) { this._loadStore(); this._loadColumnModel(); - Ext.applyIf(config,{ - store: this.store - ,cm: this.cm - ,sm: new Ext.grid.RowSelectionModel({singleSelect:true}) - ,paging: (config.bbar ? true : false) - ,loadMask: true - ,autoHeight: true - ,collapsible: true - ,stripeRows: true - ,header: false - ,cls: 'modx-grid' - ,preventRender: true - ,preventSaveRefresh: true - ,showPerPage: true - ,stateful: false - ,showActionsColumn: true - ,disableContextMenuAction: false - ,menuConfig: { - defaultAlign: 'tl-b?' - ,enableScrolling: false - } - ,viewConfig: { - forceFit: true - ,enableRowBody: true - ,autoFill: true - ,showPreview: true - ,scrollOffset: 0 - ,emptyText: config.emptyText || _('ext_emptymsg') - } - ,groupingConfig: { + Ext.applyIf(config, { + store: this.store, + cm: this.cm, + sm: new Ext.grid.RowSelectionModel({ singleSelect: true }), + // eslint-disable-next-line no-unneeded-ternary + paging: config.bbar ? true : false, + loadMask: true, + autoHeight: true, + collapsible: true, + stripeRows: true, + header: false, + cls: 'modx-grid', + preventRender: true, + preventSaveRefresh: true, + showPerPage: true, + stateful: false, + showActionsColumn: true, + disableContextMenuAction: false, + menuConfig: { + defaultAlign: 'tl-b?', + enableScrolling: false + }, + viewConfig: { + forceFit: true, + enableRowBody: true, + autoFill: true, + showPreview: true, + scrollOffset: 0, + emptyText: config.emptyText || _('ext_emptymsg') + }, + groupingConfig: { enableGroupingMenu: true } }); if (config.paging) { - var pgItms = config.showPerPage ? [_('per_page')+':',{ - xtype: 'textfield' - ,cls: 'x-tbar-page-size' - ,value: config.pageSize || (parseInt(MODx.config.default_per_page) || 20) - ,listeners: { - 'change': {fn:this.onChangePerPage,scope:this} - ,'render': {fn: function(cmp) { - new Ext.KeyMap(cmp.getEl(), { - key: Ext.EventObject.ENTER - ,fn: this.blur - ,scope: cmp - }); - },scope:this} + const pgItms = config.showPerPage ? [`${_('per_page')}:`, { + xtype: 'textfield', + cls: 'x-tbar-page-size', + value: config.pageSize || (parseInt(MODx.config.default_per_page, 10) || 20), + listeners: { + change: { + fn: this.onChangePerPage, + scope: this + }, + render: { + fn: function(cmp) { + new Ext.KeyMap(cmp.getEl(), { + key: Ext.EventObject.ENTER, + fn: this.blur, + scope: cmp + }); + }, + scope: this + } } }] : []; if (config.pagingItems) { - for (var i=0;i 1 ? "' - + (config.pluralText || _('records')) + '" : "' - + (config.singleText || _('record')) + '"]})' + const groupingConfig = { + forceFit: true, + scrollOffset: 0, + groupTextTpl: `{text} ({[values.rs.length]} {[values.rs.length > 1 ? '${config.pluralText || _('records')}' : '${config.singleText || _('record')}']})` }; Ext.applyIf(config.groupingConfig, groupingConfig); - Ext.applyIf(config,{ + Ext.applyIf(config, { view: new Ext.grid.GroupingView(config.groupingConfig) }); } if (config.tbar) { - for (var ix = 0;ix 1)) { return false; } @@ -113,10 +118,10 @@ MODx.grid.Grid = function(config = {}) { } config.columns.push({ - id: 'modx-actions' - ,width: config.actionsColumnWidth || defaultActionsColumnWidth - ,menuDisabled: true - ,renderer: this.actionsColumnRenderer.bind(this) + id: 'modx-actions', + width: config.actionsColumnWidth || defaultActionsColumnWidth, + menuDisabled: true, + renderer: this.actionsColumnRenderer.bind(this) }); } @@ -128,26 +133,28 @@ MODx.grid.Grid = function(config = {}) { } config.cm.columns.push({ - id: 'modx-actions' - ,width: config.actionsColumnWidth || defaultActionsColumnWidth - ,menuDisabled: true - ,renderer: this.actionsColumnRenderer.bind(this) + id: 'modx-actions', + width: config.actionsColumnWidth || defaultActionsColumnWidth, + menuDisabled: true, + renderer: this.actionsColumnRenderer.bind(this) }); } } - MODx.grid.Grid.superclass.constructor.call(this,config); + MODx.grid.Grid.superclass.constructor.call(this, config); this._loadMenu(config); - this.addEvents('beforeRemoveRow','afterRemoveRow','afterAutoSave'); + this.addEvents('beforeRemoveRow', 'afterRemoveRow', 'afterAutoSave'); if (this.autosave) { this.on('afterAutoSave', this.onAfterAutoSave, this); } - if (!config.preventRender) { this.render(); } + if (!config.preventRender) { + this.render(); + } this.on({ render: { fn: function() { const topToolbar = this.getTopToolbar(); - if (topToolbar && topToolbar.initialConfig.cls && topToolbar.initialConfig.cls == 'has-nested-filters') { + if (topToolbar && topToolbar.initialConfig.cls && topToolbar.initialConfig.cls === 'has-nested-filters') { this.hasNestedFilters = true; } }, @@ -159,32 +166,57 @@ MODx.grid.Grid = function(config = {}) { } }); if (config.autosave) { - this.on('afteredit',this.saveRecord,this); + this.on('afteredit', this.saveRecord, this); } if (config.paging && config.grouping) { this.getBottomToolbar().bind(this.store); } - if (!config.paging && !config.hasOwnProperty('pageSize')) { + if (!config.paging && !Object.hasOwn(config, 'pageSize')) { config.pageSize = 0; } - this.getStore().load({ params: { - start: config.pageStart || 0 - ,limit: config.hasOwnProperty('pageSize') ? config.pageSize : (parseInt(MODx.config.default_per_page) || 20) + start: config.pageStart || 0, + limit: Object.hasOwn(config, 'pageSize') ? config.pageSize : (parseInt(MODx.config.default_per_page, 10) || 20) } }); - this.getStore().on('exception',this.onStoreException,this); + this.getStore().on('exception', this.onStoreException, this); this.config = config; this.on('click', this.onClickHandler, this); }; -Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ - windows: {} +Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { + + windows: {}, + + protectedIdentifiers: null, + + /** + * The data index, not necessarily the primary key, used + * to determine if a row can be deleted / or if the value + * of the row's data index is an un-usable, reserved value + */ + protectedDataIndex: null, + + userCanEdit: false, + + userCanCreate: false, - ,onStoreException: function(dataProxy, type, action, options, response) { + userCanDelete: false, + + gridMenuActions: [], + + /** @property {Boolean} userHasPermissions Whether user has permissions of any kind to manipulate the current grid's data */ + hasPermissions: false, + + /** @property {Boolean} userHasSavePermissions Whether user has the general ability to save (to either create or edit) */ + userHasSavePermissions: false, + + showActionsMenu: null, + + onStoreException: function(dataProxy, type, action, options, response) { const responseStatusCode = response.status || 'Unknown', responseStatusText = !Ext.isEmpty(response.statusText) ? `(${response.statusText})` : '' ; @@ -223,58 +255,76 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } this.getView().emptyText = `
    ${msg}
    `; this.getView().refresh(false); - } + }, - ,saveRecord: function(e) { + /** + * Executes auto save of the row after edits are complete and optional success callback + * @param {Ext.Event} e Extended event data including: + * * column + * * row + * * field (name) + * * grid (full grid object) + * * record (full Ext record object including store, data, json, etc.) + * * originalValue + * * value (current) + */ + saveRecord: function(e) { e.record.data.menu = null; - var p = this.config.saveParams || {}; - Ext.apply(e.record.data,p); - var d = Ext.util.JSON.encode(e.record.data); - var url = this.config.saveUrl || (this.config.url || this.config.connector); + const p = this.config.saveParams || {}; + Ext.apply(e.record.data, p); + const + data = Ext.util.JSON.encode(e.record.data), + url = this.config.saveUrl || (this.config.url || this.config.connector) + ; MODx.Ajax.request({ - url: url - ,params: { - action: this.config.save_action || 'updateFromGrid' - ,data: d - } - ,listeners: { + url: url, + params: { + action: this.config.save_action || 'updateFromGrid', + data: data + }, + listeners: { success: { - fn: function(r) { + fn: function(response) { if (this.config.save_callback) { - Ext.callback(this.config.save_callback,this.config.scope || this,[r]); + Ext.callback(this.config.save_callback, this.config.scope || this, [response]); } e.record.commit(); if (!this.config.preventSaveRefresh) { const gridRefresh = new Ext.util.DelayedTask(() => this.refresh()); gridRefresh.delay(200); } - this.fireEvent('afterAutoSave',r); - } - ,scope: this - } - ,failure: { - fn: function(r) { + const + /** @var {Object} eventData Plucking only the needed event props to forward in the post-save event */ + eventData = { field: e.field, originalValue: e.originalValue, value: e.value }, + responseData = { ...response, eventData } + ; + this.fireEvent('afterAutoSave', responseData); + }, + scope: this + }, + failure: { + fn: function(response) { e.record.reject(); - this.fireEvent('afterAutoSave', r); - } - ,scope: this + this.fireEvent('afterAutoSave', response); + }, + scope: this } } }); - } + }, /** * Method executed after a record has been edited/saved inline from within the grid * * @param {Object} response - The processor save response object. See modConnectorResponse::outputContent (PHP) */ - ,onAfterAutoSave: function(response) { + onAfterAutoSave: function(response) { if (!response.success && response.message === '') { - var msg = ''; + let msg = ''; if (response.data.length) { // We get some data for specific field(s) error but not regular error message Ext.each(response.data, function(data, index, list) { - msg += (msg != '' ? '
    ' : '') + data.msg; + msg += (msg !== '' ? '
    ' : '') + data.msg; }, this); } if (Ext.isEmpty(msg)) { @@ -283,93 +333,103 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } MODx.msg.alert(_('error'), msg); } - } + }, - ,onChangePerPage: function(tf,nv) { - if (Ext.isEmpty(nv)) return false; - nv = parseInt(nv); + onChangePerPage: function(tf, nv) { + if (Ext.isEmpty(nv)) { return false; } + nv = parseInt(nv, 10); this.getBottomToolbar().pageSize = nv; - this.store.load({params:{ - start:0 - ,limit: nv - }}); - } + this.store.load({ + params: { + start: 0, + limit: nv + } + }); + }, - ,loadWindow: function(btn,e,win,or) { - var r = this.menu.record; + loadWindow: function(btn, e, win, or) { + const r = this.menu.record; if (!this.windows[win.xtype] || win.force) { - Ext.applyIf(win,{ - record: win.blankValues ? {} : r - ,grid: this - ,listeners: { - 'success': {fn:win.success || this.refresh,scope:win.scope || this} + Ext.applyIf(win, { + record: win.blankValues ? {} : r, + grid: this, + listeners: { + success: { + fn: win.success || this.refresh, + scope: win.scope || this + } } }); if (or) { - Ext.apply(win,or); + Ext.apply(win, or); } this.windows[win.xtype] = Ext.ComponentMgr.create(win); } - if (this.windows[win.xtype].setValues && win.blankValues !== true && r != undefined) { + if (this.windows[win.xtype].setValues && win.blankValues !== true && r !== undefined) { this.windows[win.xtype].setValues(r); } this.windows[win.xtype].show(e.target); - } + }, - ,confirm: function(type,text) { - var p = { action: type }; - var k = this.config.primaryKey || 'id'; + confirm: function(type, text) { + const + p = { action: type }, + k = this.config.primaryKey || 'id' + ; p[k] = this.menu.record[k]; MODx.msg.confirm({ - title: _(type) - ,text: _(text) || _('confirm_remove') - ,url: this.config.url - ,params: p - ,listeners: { - 'success': {fn:this.refresh,scope:this} + title: _(type), + text: _(text) || _('confirm_remove'), + url: this.config.url, + params: p, + listeners: { + success: { fn: this.refresh, scope: this } } }); - } + }, - ,remove: function(text, action) { + remove: function(text, action) { if (this.destroying) { return MODx.grid.Grid.superclass.remove.apply(this, arguments); } - var r = this.menu.record; + const r = this.menu.record; text = text || 'confirm_remove'; - var p = this.config.saveParams || {}; - Ext.apply(p,{ action: action || 'remove' }); - var k = this.config.primaryKey || 'id'; + const p = this.config.saveParams || {}; + Ext.apply(p, { action: action || 'remove' }); + const k = this.config.primaryKey || 'id'; p[k] = r[k]; - if (this.fireEvent('beforeRemoveRow',r)) { + if (this.fireEvent('beforeRemoveRow', r)) { MODx.msg.confirm({ - title: _('warning') - ,text: _(text, r) - ,url: this.config.url - ,params: p - ,listeners: { - 'success': {fn:function() { - this.removeActiveRow(r); - },scope:this} + title: _('warning'), + text: _(text, r), + url: this.config.url, + params: p, + listeners: { + success: { + fn: function() { + this.removeActiveRow(r); + }, + scope: this + } } }); } - } + }, - ,removeActiveRow: function(r) { - if (this.fireEvent('afterRemoveRow',r)) { - var rx = this.getSelectionModel().getSelected(); + removeActiveRow: function(r) { + if (this.fireEvent('afterRemoveRow', r)) { + const rx = this.getSelectionModel().getSelected(); this.getStore().remove(rx); } - } + }, - ,_loadMenu: function() { + _loadMenu: function() { this.menu = new Ext.menu.Menu(this.config.menuConfig); - } + }, - ,_showMenu: function(g,ri,e) { + _showMenu: function(g, ri, e) { e.stopEvent(); e.preventDefault(); this.menu.record = this.getStore().getAt(ri).data; @@ -377,41 +437,42 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ this.getSelectionModel().selectRow(ri); } this.menu.removeAll(); + let menu; if (this.getMenu) { - var m = this.getMenu(g,ri,e); - if (m && m.length && m.length > 0) { - this.addContextMenuItem(m); + menu = this.getMenu(g, ri, e); + if (menu && menu.length && menu.length > 0) { + this.addContextMenuItem(menu); } } - if ((!m || m.length <= 0) && this.menu.record.menu) { + if ((!menu || menu.length <= 0) && this.menu.record.menu) { this.addContextMenuItem(this.menu.record.menu); } if (this.menu.items.length > 0) { this.menu.showAt(e.xy); } - } + }, - ,_loadStore: function() { + _loadStore: function() { if (this.config.grouping) { this.store = new Ext.data.GroupingStore({ - url: this.config.url - ,baseParams: this.config.baseParams || { action: this.config.action || 'getList'} - ,reader: new Ext.data.JsonReader({ - totalProperty: 'total' - ,root: 'results' - ,fields: this.config.fields - }) - ,sortInfo:{ - field: this.config.sortBy || 'id' - ,direction: this.config.sortDir || 'ASC' - } - ,remoteSort: this.config.remoteSort || false - ,remoteGroup: this.config.remoteGroup || false - ,groupField: this.config.groupBy || 'name' - ,groupDir: this.config.groupDir || 'ASC' - ,storeId: this.config.storeId || Ext.id() - ,autoDestroy: true - ,listeners: { + url: this.config.url, + baseParams: this.config.baseParams || { action: this.config.action || 'getList' }, + reader: new Ext.data.JsonReader({ + totalProperty: 'total', + root: 'results', + fields: this.config.fields + }), + sortInfo: { + field: this.config.sortBy || 'id', + direction: this.config.sortDir || 'ASC' + }, + remoteSort: this.config.remoteSort || false, + remoteGroup: this.config.remoteGroup || false, + groupField: this.config.groupBy || 'name', + groupDir: this.config.groupDir || 'ASC', + storeId: this.config.storeId || Ext.id(), + autoDestroy: true, + listeners: { beforeload: function(store, options) { const changedGroupDir = store.groupField === store.sortInfo.field && store.groupDir !== store.sortInfo.direction; if (changedGroupDir) { @@ -438,15 +499,15 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ }); } else { this.store = new Ext.data.JsonStore({ - url: this.config.url - ,baseParams: this.config.baseParams || { action: this.config.action || 'getList' } - ,fields: this.config.fields - ,root: 'results' - ,totalProperty: 'total' - ,remoteSort: this.config.remoteSort || false - ,storeId: this.config.storeId || Ext.id() - ,autoDestroy: true - ,listeners:{ + url: this.config.url, + baseParams: this.config.baseParams || { action: this.config.action || 'getList' }, + fields: this.config.fields, + root: 'results', + totalProperty: 'total', + remoteSort: this.config.remoteSort || false, + storeId: this.config.storeId || Ext.id(), + autoDestroy: true, + listeners: { load: function() { const cmp = Ext.getCmp('modx-content'); if (cmp) { @@ -456,25 +517,27 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } }); } - } + }, - ,_loadColumnModel: function() { + _loadColumnModel: function() { if (this.config.columns) { - var c = this.config.columns; - for (var i=0;i item.trim()); + columnIds.forEach(colId => { + const colIndex = colModel.getIndexById(colId); + colModel.setEditable(colIndex, false); + }); + } + }, + + /* User Group-Level Permissions Checks for the calling "class" object */ + + /** + * @property {Function} setUserCanEdit - Assigns a value to userCanEdit property based on + * the user's permissions; used to adjust which menu items are available, whether to render links + * to and item's editing page, and css cues across many grid classes + * + * @param {Array} groupPermissions - A set of permissions keys to evaluate; note that many areas currently + * rely on a pair of permissions (save_x and edit_x), both of which must be enabled to edit a grid item + * + * @return void + */ + setUserCanEdit: function(groupPermissions) { + groupPermissions = groupPermissions.map(item => item.trim()); + this.userCanEdit = groupPermissions.every(permission => MODx.perm[permission]); + if (this.userCanEdit) { + this.userHasPermissions = true; + } + }, + + /** + * @property {Function} setUserCanCreate - Assigns a value to userCanCreate property based on + * the user's permissions; used to adjust which menu items are available (namely the Duplicate item) + * and whether to render the Create button in the grid's toolbar + * + * @param {Array} groupPermissions - A set of permissions keys to evaluate; note that many areas currently + * rely on a pair of permissions (save_x and new_x), both of which must be enabled to create/duplicate a grid item + * + * @return void + */ + setUserCanCreate: function(groupPermissions) { + groupPermissions = groupPermissions.map(item => item.trim()); + this.userCanCreate = groupPermissions.every(permission => MODx.perm[permission]); + if (this.userCanCreate) { + this.userHasPermissions = true; + } + }, + + /** + * @property {Function} setUserCanDelete - Assigns a value to userCanDelete property based on + * the user's permissions; used to adjust which menu items are available in the context menus + * and whether to render the Delete menu item within a grid toolbar's Batch button + * + * @param {Array} groupPermissions - A set of permissions keys to evaluate + * + * @return void + */ + setUserCanDelete: function(groupPermissions) { + groupPermissions = groupPermissions.map(item => item.trim()); + this.userCanDelete = groupPermissions.every(permission => MODx.perm[permission]); + if (this.userCanDelete) { + this.userHasPermissions = true; + } + }, + + /* Record-Level Permissions Checks, for objects with specific policies */ + + userHasRecordPermissions: function(record) { + const objPermissions = record.json.permissions; + if (Ext.isEmpty(objPermissions)) { + return false; + } + return Object.values(objPermissions).some(permission => Boolean(permission) === true); + }, + + userCanEditRecord: function(record) { + const objPermissions = record.json.permissions; + return !Ext.isEmpty(objPermissions) && objPermissions.update === true; + }, + + userCanDeleteRecord: function(record) { + const objPermissions = record.json.permissions; + return !Ext.isEmpty(objPermissions) && !record.json.isProtected && objPermissions.delete === true; + }, - ,renderEditableColumn: function(renderer) { + userCanDuplicateRecord: function(record) { + const objPermissions = record.json.permissions; + return !Ext.isEmpty(objPermissions) && objPermissions.duplicate === true; + }, + + /** + * @property {Function} setShowActionsMenu - Based on properties set in the calling child class and the + * the current user's permissions for actions taken within that class (create, edit, delete, etc), + * evaluates whether the actions menu trigger should appear and sets boolean value on the showActionsMenu property + * + * @return void + */ + setShowActionsMenu: function() { + if (this.config.disableContextMenuAction === true) { + this.showActionsMenu = false; + return; + } + const permissionsValues = []; + this.gridMenuActions.forEach(mode => { + mode = mode === 'duplicate' ? 'userCanCreate' : `userCan${Ext.util.Format.capitalize(mode)}`; + const modePermission = mode === 'userCanExport' ? true : this[mode]; + if (['userCanCreate', 'userCanEdit'].includes(mode) && modePermission === true) { + this.userHasSavePermissions = true; + } + permissionsValues.push(modePermission); + }); + this.showActionsMenu = !(permissionsValues.length === 0 || permissionsValues.every(value => value === false) === true); + }, + + /** + * @property {Function} recordIsProtected - Used to remove the ability to delete + * specific record rows, regardless of permissions levels, based on a given record identifier + * + * @param {Number} subject - The value of the current record's identifier + * @param {Number} protectedIdentifiers - The record identifiers to be protected (making them non-editable/deletable) + * + * @return {Boolean} + */ + recordIsProtected: function(subject, protectedIdentifiers) { + if (Ext.isEmpty(protectedIdentifiers)) { + return false; + } + protectedIdentifiers = protectedIdentifiers.map(identifier => (typeof identifier === 'string' ? identifier.trim() : identifier)); + return protectedIdentifiers.includes(subject); + }, + + /** + * @property {Function} valueIsReserved - Wraps a grid value with a real or simulated link — a trigger that appears + * like an anchor link, usually to access a dropdown chooser or other control + * + * @param {Array|String} reservedValues - A set of values that can not be used for a particular object's field + * @param {Object} value - The submitted value being tested + * + * @return {Boolean} + */ + valueIsReserved: function(reservedValues, value) { + if (!Array.isArray(reservedValues)) { + reservedValues = reservedValues.split(','); + } + return reservedValues.some(reserved => reserved.toLowerCase() === value.toLowerCase()); + }, + + /** + * @property {Function} getRemovableItemsFromSelection - Prunes protected items from the current + * selection list before submitting for deletion, or for setting the state of the 'Delete Selected' + * menu item + * + * @param {String} itemIdType - The data type of the value being inspected (either string or integer) + * + * @return {Array} + */ + getRemovableItemsFromSelection: function(itemIdType = 'int') { + const selections = this.getSelectionModel().getSelections(), + pk = this.config.primaryKey || 'id', + removableItems = [] + ; + if (selections.length <= 0) { + return []; + } + selections.forEach(record => { + const deletableRecord = record.json.permissions.delete; + if (!record.json.isProtected && deletableRecord) { + const item = itemIdType === 'string' ? record.data[pk] : parseInt(record.data[pk], 10); + removableItems.push(item); + } + }); + return removableItems; + }, + + renderEditableColumn: function(renderer) { return function(value, metaData, record, rowIndex, colIndex, store) { if (renderer) { if (typeof renderer.fn === 'function') { - var scope = (renderer.scope) ? renderer.scope : false; + const scope = (renderer.scope) ? renderer.scope : false; renderer = renderer.fn.bind(scope); } - if (typeof renderer === 'function') { value = renderer(value, metaData, record, rowIndex, colIndex, store); } @@ -581,148 +829,199 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ metaData.css = ['x-editable-column', metaData.css || ''].join(' '); return value; - } - } + }; + }, - ,rendYesNo: function(v,md) { - if (v === 1 || v == '1') { v = true; } - if (v === 0 || v == '0') { v = false; } + rendYesNo: function(v, metaData) { + if (v === 1 || v === '1') { v = true; } + if (v === 0 || v === '0') { v = false; } switch (v) { case true: case 'true': case 1: - md.css = 'green'; + metaData.css = 'green'; return _('yes'); case false: case 'false': case '': case 0: - md.css = 'red'; + metaData.css = 'red'; return _('no'); // no default } - } + }, - ,getSelectedAsList: function() { - var sels = this.getSelectionModel().getSelections(); - if (sels.length <= 0) return false; + /* Depricated; remove once all grids with bulk deletion capability have been converted */ + getSelectedAsList: function() { + const sels = this.getSelectionModel().getSelections(); + if (sels.length <= 0) { return false; } - var cs = ''; - for (var i=0;i' + '' + '
      ' @@ -734,10 +1033,49 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ + '', { compiled: true }); - } + }, + + actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { + /* + Note: To maintain backward compatibility for core grids that have not yet been updated + to the new permissions checks and for extras that may extend this class in their grids, + we check showActionsMenu for strict boolean values (which will only be set by grids using + the new checks); otherwise showActionsMenu will be null (its default value set above), + indicating the legacy checks are to be used. + */ + if (this.showActionsMenu === false) { + return; + } + /* + showActionsMenu will be true if at least one user group-level permission is granted, + excluding create/new permissions (since that is not executed by our context/actions menus). + */ + if (this.showActionsMenu) { + const { isProtected } = record.json; + // Export is always available; only continue filtering if grid does not offer export + if (!this.gridMenuActions.includes('export')) { + if (!this.userHasSavePermissions && isProtected) { + return; + } + // Checking record-level permissions; this block checking for 'cls' can be removed once all grids are updated + if (Object.hasOwn(record.data, 'cls')) { + if (Ext.isEmpty(record.data.cls)) { + return; + } + } + if (Object.hasOwn(record.json, 'permissions')) { + if ( + Ext.isEmpty(record.json.permissions) + || Object.values(record.json.permissions).every(permission => !permission) + ) { + return; + } + } + } + } + // eslint-disable-next-line prefer-spread + const actions = this.getActions.apply(this, arguments); - ,actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { - var actions = this.getActions.apply(this, [record, rowIndex, colIndex, store]); if (this.config.disableContextMenuAction !== true) { actions.push({ text: _('context_menu'), @@ -745,36 +1083,53 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ icon: 'gear' }); } - return this._getActionsColumnTpl().apply({ actions: actions }); - } + }, - ,renderLink: function(v,attr) { - var el = new Ext.Element(document.createElement('a')); - el.addClass('x-grid-link'); - el.dom.title = _('edit'); - for (var i in attr) { - el.dom[i] = attr[i]; + /** + * @property {Function} renderLink - Wraps a grid value with a real or simulated link — a trigger that appears + * like an anchor link, usually to access a dropdown chooser or other control + * + * @param {String} content - The value being wrapped + * @param {Object} attributes - Html attributes to add to the link's tag + * @param {Boolean} isSimulated - Indicates whether the link is real (anchor tag) or not (simulated) + * @param {String} isSimulatedTag - The html tag name to wrap the content with + * + * @return {String} + */ + renderLink: function(content, attributes = {}, isSimulated = false, isSimulatedTag = 'span') { + const + tag = isSimulated ? isSimulatedTag : 'a', + classes = isSimulated ? 'x-grid-link simulated-link' : 'x-grid-link', + el = new Ext.Element(document.createElement(tag)) + ; + el.addClass(classes); + // Add default title if none given in attributes + if (!Object.hasOwn(attributes, 'title')) { + attributes.title = _('edit'); } - el.dom.innerHTML = Ext.util.Format.htmlEncode(v); + Object.entries(attributes).forEach(([attr, value]) => { + el.dom[attr] = value; + }); + el.dom.innerHTML = Ext.util.Format.htmlEncode(content); return el.dom.outerHTML; - } + }, /** * Deprecated; renamed checkCellIsEditable. Remove in 3.1 */ - ,checkEditable: function(e) { + checkEditable: function(e) { this.checkCellIsEditable(e); - } + }, /** * Disables cell editor under specified conditions * @param {Object} e - Ext event object containing references to grid, record, field, value, row (index), column (index), and cancel (set true to cancel edit). * @return {Boolean} Return false to cancel or true to commit the edit */ - ,checkCellIsEditable: function(e) { + checkCellIsEditable: function(e) { const permissions = e.record.data.perm || ''; if (permissions.indexOf('edit') === -1) { return false; @@ -794,20 +1149,20 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ default: } return true; - } + }, /** * Add one or more classes to a specific Editor Grid cell, typically to indicate a level of restriction - * + * * @param {Object} record - The row's data record * @param {Array} lockConditions - A set of one or more Boolean values (or ones that cast correctly to the expected Boolean value) derived from the row record or other values that indicate whether or not the subject cell should be marked as locked * @param {String} lockedClasses - One or more css class names * @param {Boolean} conditionsRequireAll - Whether all passed lockConditions need to evaluate to true to apply the locked class(es) */ - ,setEditableCellClasses: function(record, lockConditions = [], lockedClasses = 'locked', conditionsRequireAll = true) { + setEditableCellClasses: function(record, lockConditions = [], lockedClasses = '', conditionsRequireAll = true) { const - permissions = record.data.perm.trim(), - hasEditPermission = permissions.split(' ').includes('edit') + userCanEditRecord = this.userCanEditRecord(record), + lockedCSS = lockedClasses || 'locked' ; let classes = '', @@ -819,13 +1174,13 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ : lockConditions.some(condition => Boolean(condition) === true) ; } - if (Ext.isEmpty(permissions)) { + if (!this.userCanEdit || !this.userHasRecordPermissions(record) || !userCanEditRecord) { classes = 'editor-disabled'; - } else if (hasEditPermission && shouldLock) { - classes = lockedClasses; + } else if (userCanEditRecord && shouldLock) { + classes = lockedCSS; } return classes; - } + }, /** * @property {Function} getLinkTemplate - Adds a link on a grid column's value based on the passed params. @@ -837,7 +1192,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {Object} options - Additional URL query parameters (linkParams) and attributes for the link's anchor tag * @return {Ext.Template} */ - ,getLinkTemplate: function(controllerPath, displayValueIndex, options = {}) { + getLinkTemplate: function(controllerPath, displayValueIndex, options = {}) { /* linkParams, if given, should be an array of objects in the following format: [{ key: 'paramKey', valueIndex: 'paramValue' }, ...{}] @@ -849,7 +1204,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ linkTarget: '_blank' }); let params = ''; - controllerPath = controllerPath.indexOf('?a=') === 0 ? controllerPath : `?a=${controllerPath}` ; + controllerPath = controllerPath.indexOf('?a=') === 0 ? controllerPath : `?a=${controllerPath}`; if (options.linkParams.length > 0) { params = []; options.linkParams.forEach(param => { @@ -861,18 +1216,18 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ `{${displayValueIndex}:htmlEncode}`, { compiled: true } ); - } + }, - ,getActions: function(record, rowIndex, colIndex, store) { + getActions: function(value, metaData, record, rowIndex, colIndex, store) { return []; - } + }, - ,onClickHandler: function(e) { - var target = e.getTarget(); - if (!target.classList.contains('x-grid-action')) return; - if (!target.dataset.action) return; + onClickHandler: function(e) { + const target = e.getTarget(); + if (!target.classList.contains('x-grid-action')) { return; } + if (!target.dataset.action) { return; } - var actionHandler = 'action' + target.dataset.action.charAt(0).toUpperCase() + target.dataset.action.slice(1); + let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { actionHandler = target.dataset.action; if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { @@ -880,33 +1235,33 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } } - var record = this.getSelectionModel().getSelected(); - var recordIndex = this.store.indexOf(record); + const record = this.getSelectionModel().getSelected(), + recordIndex = this.store.indexOf(record); this.menu.record = record.data; this[actionHandler](record, recordIndex, e); - } + }, - ,actionContextMenu: function(record, recordIndex, e) { + actionContextMenu: function(record, recordIndex, e) { this._showMenu(this, recordIndex, e); - } + }, - ,makeUrl: function () { + makeUrl: function() { if (Array.isArray(this.config.urlFilters) && this.config.urlFilters.length > 0) { - var s = this.getStore(); - var p = { - a: MODx.request.a - } + const s = this.getStore(), + p = { + a: MODx.request.a + }; if (MODx.request.id) { - p['id'] = MODx.request.id; + p.id = MODx.request.id; } if (MODx.request.key) { - p['key'] = MODx.request.key; + p.key = MODx.request.key; } - for (var i = 0; i < this.config.urlFilters.length; ++i) { - if (s.baseParams.hasOwnProperty(this.config.urlFilters[i]) && s.baseParams[this.config.urlFilters[i]]) { + for (let i = 0; i < this.config.urlFilters.length; ++i) { + if (Object.hasOwn(s.baseParams, this.config.urlFilters[i]) && s.baseParams[this.config.urlFilters[i]]) { if (this.config.urlFilters[i] === 'namespace') { - p['ns'] = s.baseParams[this.config.urlFilters[i]]; + p.ns = s.baseParams[this.config.urlFilters[i]]; } else { p[this.config.urlFilters[i]] = s.baseParams[this.config.urlFilters[i]]; } @@ -914,15 +1269,16 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } return Ext.urlAppend(MODx.config.manager_url, Ext.urlEncode(p).replace(/%2F/g, '/')); } - } + }, - ,replaceState: function () { - if (typeof window.history.replaceState !== 'undefined' && - Array.isArray(this.config.urlFilters) && this.config.urlFilters.length > 0 + replaceState: function() { + if (typeof window.history.replaceState !== 'undefined' + && Array.isArray(this.config.urlFilters) + && this.config.urlFilters.length > 0 ) { window.history.replaceState(this.getStore().baseParams, document.title, this.makeUrl()); } - } + }, /** * @property {Function} findTabPanel - Recursively search ownerCts for this component's enclosing TabPanel @@ -930,28 +1286,28 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {Object} referenceCmp - A child component of the TabPanel we're looking for * @return Ext.TabPanel */ - ,findTabPanel: function(referenceCmp) { - if (!referenceCmp.hasOwnProperty('ownerCt')) { + findTabPanel: function(referenceCmp) { + if (!Object.hasOwn(referenceCmp, 'ownerCt')) { console.error('MODx.grid.Grid::findTabPanel: This component must have an ownerCt to find its tab panel.'); return false; } const container = referenceCmp.ownerCt, - isTabPanel = container.hasOwnProperty('xtype') && container.xtype.includes('tabs') + isTabPanel = Object.hasOwn(container, 'xtype') && container.xtype.includes('tabs') ; if (isTabPanel) { return container; } return this.findTabPanel(container); - } + }, /** * @property {Boolean} hasNestedFilters - Indicates whether the top toolbar filter(s) are nested * within a secondary container; they will be nested when they have labels and those labels are * positioned above the filter's input. */ - ,hasNestedFilters: false + hasNestedFilters: false, - ,currentLanguage: MODx.config.cultureKey || 'en' // removed MODx.request.language + currentLanguage: MODx.config.cultureKey || 'en', // removed MODx.request.language /** * Applies a value persisted via URL (MODx.request) for use in grid and filter params. Used when multiple @@ -965,15 +1321,15 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * when no value is present. Set this to true for components that prefer an empty string * @returns {Number|String} Decoded param value */ - ,applyRequestFilter: function(tabPanelIndex, requestKey = 'policy', tabPanelType = 'vtab', setEmptyToString = false) { - const emptyVal = setEmptyToString ? '' : null ; + applyRequestFilter: function(tabPanelIndex, requestKey = 'policy', tabPanelType = 'vtab', setEmptyToString = false) { + const emptyVal = setEmptyToString ? '' : null; return Object.prototype.hasOwnProperty.call(MODx.request, tabPanelType) && parseInt(MODx.request[tabPanelType], 10) === tabPanelIndex && Object.prototype.hasOwnProperty.call(MODx.request, requestKey) - ? MODx.util.url.getParamValue(requestKey) - : emptyVal + ? MODx.util.url.getParamValue(requestKey) + : emptyVal ; - } + }, /** * Filters the grid data by the passed filter component (field) @@ -982,7 +1338,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {String} param - The record index to apply the filter on; * may also be the general query/search field name. */ - ,applyGridFilter: function(cmp, param = 'query') { + applyGridFilter: function(cmp, param = 'query') { const filterValue = cmp.getValue(), store = this.getStore(), urlParams = {}, @@ -1005,8 +1361,8 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } else { MODx.util.url.clearParam(cmp); } - if (param == 'ns') { - store.baseParams['namespace'] = filterValue; + if (param === 'ns') { + store.baseParams.namespace = filterValue; } else { store.baseParams[param] = filterValue; } @@ -1015,7 +1371,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ Determine if this is a vertical tab panel; if so there will also be a horizontal parent tab panel that needs to be accounted for */ - if (tabPanel.xtype == 'modx-vtabs') { + if (tabPanel.xtype === 'modx-vtabs') { const parentTabPanel = this.findTabPanel(tabPanel); if (parentTabPanel) { const activeParentTab = parentTabPanel.getActiveTab(); @@ -1036,18 +1392,16 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ if (parentTabItems.length > 1) { urlParams.tab = activeParentTabIdx; } - } else { - if (tabItems.length > 1) { - urlParams.tab = activeTabIdx; - } + } else if (tabItems.length > 1) { + urlParams.tab = activeTabIdx; } } store.load(); - MODx.util.url.setParams(urlParams) + MODx.util.url.setParams(urlParams); if (bottomToolbar) { bottomToolbar.changePage(1); } - } + }, /** * @property {Function} clearGridFilters - Clears all grid filters and sets them to their default value @@ -1058,7 +1412,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * 'filter-category:3', where '3' is the filter's default value to be applied (instead of setting to an empty value) * */ - ,clearGridFilters: function(items) { + clearGridFilters: function(items) { const store = this.getStore(), bottomToolbar = this.getBottomToolbar(), data = Array.isArray(items) ? items : items.split(',') @@ -1066,9 +1420,9 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ data.forEach(item => { const itemData = item.replace(/\s+/g, '').split(':'), itemId = itemData[0], - itemDefaultVal = itemData.length == 2 ? itemData[1] : null , + itemDefaultVal = itemData.length === 2 ? itemData[1] : null, cmp = this.getFilterComponent(itemId), - param = MODx.util.url.getParamNameFromCmp(cmp), + cmpParam = MODx.util.url.getParamNameFromCmp(cmp), isCombo = cmp?.xtype?.includes('combo') ; if (isCombo) { @@ -1083,13 +1437,14 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ if (!Ext.isEmpty(itemDefaultVal)) { const paramsList = Object.keys(cmp.baseParams); paramsList.forEach(param => { - switch(param) { + switch (param) { case 'namespace': cmp.baseParams[param] = 'core'; break; case 'topic': cmp.baseParams[param] = 'default'; break; + // no default } }); } @@ -1098,21 +1453,14 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ cmp.getStore().load(); } } - store.baseParams[param] = itemDefaultVal; + store.baseParams[cmpParam] = itemDefaultVal; }); store.load(); MODx.util.url.clearAllParams(); if (bottomToolbar) { bottomToolbar.changePage(1); } - } - - /** - * @property {Boolean} hasNestedFilters - Indicates whether the top toolbar filter(s) are nested - * within a secondary container; they will be nested when they have labels and those labels are - * positioned above the filter's input. - */ - ,hasNestedFilters: false + }, /** * @property {Function} getFilterComponent - Gets a filter component from the top toolbar by its itemId @@ -1120,17 +1468,17 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {String} filterId - The Ext itemId of the filter component to fetch * @return {Ext.Component} */ - ,getFilterComponent: function(filterId) { + getFilterComponent: function(filterId) { const topToolbar = this.getTopToolbar(), cmp = this.hasNestedFilters && filterId !== 'filter-query' - ? topToolbar.find('itemId', `${filterId}-container`)[0].getComponent(filterId) - : topToolbar.getComponent(filterId) + ? topToolbar.find('itemId', `${filterId}-container`)[0].getComponent(filterId) + : topToolbar.getComponent(filterId) ; if (typeof cmp !== 'undefined') { return cmp; } console.error(`getFilterComponent: The filter component with itemId '${filterId}' could not be retrieved.`); - } + }, /** * @property {Function} refreshFilterOptions - Used to syncronize a filter's store options to those available in its target grid @@ -1138,7 +1486,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {Array} filterData - An array of objects containing info needed to refresh each filter * @param {Boolean} clearDependentParams - If true, will clear values of dependentParams specified in the filterData */ - ,refreshFilterOptions: function(filterData = [], clearDependentParams = true) { + refreshFilterOptions: function(filterData = [], clearDependentParams = true) { if (filterData.length > 0) { filterData.forEach(data => { const filter = this.getFilterComponent(data.filterId); @@ -1146,10 +1494,13 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ const store = filter.getStore(); filter.setValue(''); if (store) { - if (data.hasOwnProperty('dependentParams')) { - const dependentParams = Array.isArray(data.dependentParams) ? data.dependentParams : data.dependentParams.split(','); + if (Object.hasOwn(data, 'dependentParams')) { + const dependentParams = Array.isArray(data.dependentParams) + ? data.dependentParams + : data.dependentParams.split(',') + ; dependentParams.forEach(param => { - if (clearDependentParams && store.baseParams.hasOwnProperty(param)) { + if (clearDependentParams && Object.hasOwn(store.baseParams, param)) { store.baseParams[param] = ''; } }); @@ -1158,9 +1509,9 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } } }); - this.refresh(); + this.refresh(); } - } + }, /** * @property {Function} updateDependentFilter - Reloads a related filter's store based on the current filter's selected item @@ -1170,7 +1521,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {Mixed} paramValue - Filter baseParams value for the paramKey * @param {Boolean} clearValue - Set true to clear filter's selected value */ - ,updateDependentFilter: function(filterId, paramKey, paramValue, clearValue = false) { + updateDependentFilter: function(filterId, paramKey, paramValue, clearValue = false) { const filter = this.getFilterComponent(filterId), filterStore = filter ? filter.getStore() : null ; @@ -1181,7 +1532,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ filterStore.baseParams[paramKey] = paramValue; filterStore.load(); } - } + }, /** * @property {Function} getQueryFilterField - Creates the query field component configuration @@ -1192,16 +1543,17 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {String} implementation - Optional, an identifier used to assign grid-specific behavior * @return {Object} */ - ,getQueryFilterField: function(filterSpec = 'filter-query', implementation = 'default') { + getQueryFilterField: function(filterSpec = 'filter-query', implementation = 'default') { let queryValue = ''; const filterSpecs = filterSpec.split(':'), filterId = filterSpecs[0].trim() ; if (filterSpecs.length === 2) { + // eslint-disable-next-line prefer-destructuring queryValue = filterSpecs[1]; } else { - queryValue = MODx.request.query ? MODx.util.url.decodeParamValue(MODx.request.query) : '' ; + queryValue = MODx.request.query ? MODx.util.url.decodeParamValue(MODx.request.query) : ''; } return { xtype: 'textfield', @@ -1213,7 +1565,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ change: { fn: function(cmp, newValue, oldValue) { this.applyGridFilter(cmp); - const usergroupTree = Ext.getCmp('modx-tree-usergroup') + const usergroupTree = Ext.getCmp('modx-tree-usergroup'); if (implementation === 'user-group-users' && usergroupTree) { /* When the user group users grid is shown in the primary ACLs panel, @@ -1223,7 +1575,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ const selectedNode = usergroupTree.getSelectionModel().getSelectedNode(), groupId = MODx.util.tree.getGroupIdFromNode(selectedNode) ; - MODx.util.url.setParams({group: groupId}); + MODx.util.url.setParams({ group: groupId }); } }, scope: this @@ -1243,12 +1595,12 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ fn: this.blur, scope: cmp }); - } - ,scope: this + }, + scope: this } } - } - } + }; + }, /** * @property {Function} getClearFiltersButton - Creates the clear filter button component configuration @@ -1258,7 +1610,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * in the following format: 'filterItemId:relatedBaseParam, [filterItemId:relatedBaseParam,] ...' * @return {Object} */ - ,getClearFiltersButton: function(filters = 'filter-query', dependentFilterResets = null) { + getClearFiltersButton: function(filters = 'filter-query', dependentFilterResets = null) { if (Ext.isEmpty(filters)) { console.error('MODx.grid.Grid::getClearFiltersButton: There was a problem creating the Clear Filter button because the supplied filters list is invalid.'); return {}; @@ -1286,61 +1638,204 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } } } - } + }; if (dependentFilterResets) { config.dependentResets = dependentFilterResets; } return config; + }, + + /** + * Builds the standard "Creator" column model object. This column displays for + * objects that have built-in system values as well as values installed/entered + * by Extras and/or Users + * @param {String} objectType Identifier for object being worked with + * @returns {Object} The configuration for the "Creator" column + */ + getCreatorColumnConfig: function(objectType) { + return { + header: _('grid_column_creator_header'), + dataIndex: 'creator', + id: `modx-${objectType}--creator`, + width: 70, + align: 'center', + tooltip: _('grid_column_creator_description'), + menuDisabled: true + }; + }, + + /** + * Builds the bulk actions button, containing a menu of various actions + * (typically only contains a delete action) + * @param {String} objectType Identifier for object being worked with + * @param {String} deleteAction Processor path for the removal action + * @param {String} pkType Specifies the object's primary key type (int or string) + * @param {...any} moreActions Additional button identifiers or config objects + * to add to the bulk actions menu + * @returns {Object} The complete bulk actions config + */ + getBulkActionsButton: function(objectType, deleteAction, pkType = 'int', ...moreActions) { + const + menuItems = [], + additionalMenuItems = [], + hasMoreActions = moreActions.length > 0 + ; + if (hasMoreActions) { + /** @var standardButtons Button configs for actions that are used in select grids, such as the Users and Form Customization (Sets) grids */ + const standardButtons = { + activate: { + text: _('selected_activate'), + itemId: 'modx-bulk-menu-opt-activate', + handler: this.activateSelected, + scope: this + }, + deactivate: { + text: _('selected_deactivate'), + itemId: 'modx-bulk-menu-opt-deactivate', + handler: this.deactivateSelected, + scope: this + } + }; + moreActions.forEach(action => { + if (typeof action === 'string') { + const key = action.toLowerCase(); + if (Object.hasOwn(standardButtons, key)) { + additionalMenuItems.push(standardButtons[key]); + } + } + }); + menuItems.push(...additionalMenuItems); + menuItems.push('-'); + } + menuItems.push({ + text: _('selected_remove'), + itemId: 'modx-bulk-menu-opt-remove', + handler: this.removeSelected.createDelegate(this, [objectType, deleteAction, pkType]), + scope: this + }); + return { + text: _('bulk_actions'), + menu: menuItems, + listeners: { + render: { + fn: function(btn) { + if (!this.userCanDelete && !hasMoreActions) { + btn.hide(); + } + }, + scope: this + }, + click: { + fn: function(btn) { + const + removableItems = this.getRemovableItemsFromSelection(pkType), + menuOptRemove = btn.menu.getComponent('modx-bulk-menu-opt-remove') + ; + if (removableItems.length === 0) { + menuOptRemove.disable(); + } else { + menuOptRemove.enable(); + } + if (hasMoreActions) { + const selections = this.getSelectionModel().getSelections(); + additionalMenuItems.forEach(item => { + const itemCmp = btn.menu.getComponent(item.itemId); + if (selections.length === 0) { + itemCmp.disable(); + } else { + itemCmp.enable(); + } + }); + } + }, + scope: this + } + } + }; + }, + + /** + * Gets the view configuration for grids having row-specific editing permissions + * @param {Boolean} hasBulkActions Whether the grid has a bulk actions option + * (uses the checkbox selection model to select multiple rows) + * @param {Boolean} hasObjectLevelPermissions Whether individual rows might have + * differing permissions, based on the specific object they represent + * @returns {Object} The complete view config + */ + getViewConfig: function(hasBulkActions = true, hasObjectLevelPermissions = true) { + return { + forceFit: true, + scrollOffset: 0, + getRowClass: function(record, index, rowParams, store) { + // Adds the returned class to the row container's css classes + if (hasObjectLevelPermissions && this.grid.userCanDeleteRecord(record)) { + return ''; + } + const rowClasses = hasBulkActions ? 'disable-selection' : '' ; + return record.json.isProtected ? `modx-protected-row ${rowClasses}` : rowClasses ; + } + }; } }); -/* local grid */ -MODx.grid.LocalGrid = function(config) { - config = config || {}; - +/* + Local Grid, used by: + - FC Profile Set TVs grid + - Element Properties grid + - Element Sources grid + - Source Properties + - Source Access Permissions + - Resource, Resource Groups (security) grid + - User, Access Permissions (user-groups) + - Dashboard Widget, Dashboards grid (modx-grid-dashboard-widget-dashboards) + - Dashboards (modx-grid-dashboard-widget-placements) +*/ +MODx.grid.LocalGrid = function(config = {}) { if (config.grouping) { - Ext.applyIf(config,{ + Ext.applyIf(config, { view: new Ext.grid.GroupingView({ - forceFit: true - ,scrollOffset: 0 - ,hideGroupedColumn: config.hideGroupedColumn ? true : false - ,groupTextTpl: config.groupTextTpl || ('{text} ({[values.rs.length]} {[values.rs.length > 1 ? "' - +(config.pluralText || _('records')) + '" : "' - +(config.singleText || _('record'))+'"]})' ) + forceFit: true, + scrollOffset: 0, + hideGroupedColumn: config.hideGroupedColumn, + groupTextTpl: config.groupTextTpl || (`{text} ({[values.rs.length]} {[values.rs.length > 1 ? "${ + config.pluralText || _('records')}" : "${ + config.singleText || _('record')}"]})`) }) }); } if (config.tbar) { - for (var i = 0;i' + '' + '
        ' @@ -1683,10 +2181,11 @@ Ext.extend(MODx.grid.LocalGrid,Ext.grid.EditorGridPanel,{ + '', { compiled: true }); - } + }, - ,actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { - var actions = this.getActions.apply(this, arguments); + actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { + // eslint-disable-next-line prefer-spread + const actions = this.getActions.apply(this, arguments); if (this.config.disableContextMenuAction !== true) { actions.push({ @@ -1699,29 +2198,31 @@ Ext.extend(MODx.grid.LocalGrid,Ext.grid.EditorGridPanel,{ return this._getActionsColumnTpl().apply({ actions: actions }); - } + }, - ,renderLink: function(v,attr) { - var el = new Ext.Element(document.createElement('a')); + renderLink: function(content, attributes) { + const el = new Ext.Element(document.createElement('a')); el.addClass('x-grid-link'); - el.dom.title = _('edit'); - for (var i in attr) { - el.dom[i] = attr[i]; + if (!Object.hasOwn(attributes, 'title')) { + attributes.title = _('edit'); } - el.dom.innerHTML = Ext.util.Format.htmlEncode(v); + Object.entries(attributes).forEach(([attr, value]) => { + el.dom[attr] = value; + }); + el.dom.innerHTML = Ext.util.Format.htmlEncode(content); return el.dom.outerHTML; - } + }, - ,getActions: function(value, metaData, record, rowIndex, colIndex, store) { + getActions: function(value, metaData, record, rowIndex, colIndex, store) { return []; - } + }, - ,onClick: function(e) { - var target = e.getTarget(); - if (!target.classList.contains('x-grid-action')) return; - if (!target.dataset.action) return; + onClick: function(e) { + const target = e.getTarget(); + if (!target.classList.contains('x-grid-action')) { return; } + if (!target.dataset.action) { return; } - var actionHandler = 'action' + target.dataset.action.charAt(0).toUpperCase() + target.dataset.action.slice(1); + let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { actionHandler = target.dataset.action; if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { @@ -1729,8 +2230,8 @@ Ext.extend(MODx.grid.LocalGrid,Ext.grid.EditorGridPanel,{ } } - var record = this.getSelectionModel().getSelected(); - var recordIndex = this.store.indexOf(record); + const record = this.getSelectionModel().getSelected(), + recordIndex = this.store.indexOf(record); this.menu.record = record.data; this[actionHandler](record, recordIndex, e); @@ -1740,8 +2241,8 @@ Ext.extend(MODx.grid.LocalGrid,Ext.grid.EditorGridPanel,{ this._showMenu(this, recordIndex, e); } }); -Ext.reg('grid-local',MODx.grid.LocalGrid); -Ext.reg('modx-grid-local',MODx.grid.LocalGrid); +Ext.reg('grid-local', MODx.grid.LocalGrid); +Ext.reg('modx-grid-local', MODx.grid.LocalGrid); /* grid extensions */ /*! @@ -1767,26 +2268,26 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { * true to toggle selected row(s) between expanded/collapsed when the enter * key is pressed (defaults to true). */ - expandOnEnter : true, + expandOnEnter: true, /** * @cfg {Boolean} expandOnDblClick * true to toggle a row between expanded/collapsed when double clicked * (defaults to true). */ - expandOnDblClick : true, + expandOnDblClick: true, - header : '', - width : 20, - sortable : false, - fixed : true, + header: '', + width: 20, + sortable: false, + fixed: true, hideable: false, - menuDisabled : true, - dataIndex : '', - id : 'expander', - lazyRender : true, - enableCaching : true, + menuDisabled: true, + dataIndex: '', + id: 'expander', + lazyRender: true, + enableCaching: true, - constructor: function(config){ + constructor: function(config) { Ext.apply(this, config); this.addEvents({ @@ -1830,8 +2331,8 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { Ext.ux.grid.RowExpander.superclass.constructor.call(this); - if(this.tpl){ - if(typeof this.tpl == 'string'){ + if (this.tpl) { + if (typeof this.tpl == 'string') { this.tpl = new Ext.Template(this.tpl); } this.tpl.compile(); @@ -1841,39 +2342,40 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { this.bodyContent = {}; }, - getRowClass : function(record, rowIndex, p, ds){ - p.cols = p.cols-1; - var content = this.bodyContent[record.id]; - if(!content && !this.lazyRender){ + getRowClass: function(record, rowIndex, p, ds) { + p.cols -= 1; + let content = this.bodyContent[record.id]; + if (!content && !this.lazyRender) { content = this.getBodyContent(record, rowIndex); } - if(content){ + if (content) { p.body = content; } return this.state[record.id] ? 'x-grid3-row-expanded' : 'x-grid3-row-collapsed'; }, - init : function(grid){ + init: function(grid) { this.grid = grid; - var view = grid.getView(); + const view = grid.getView(); view.getRowClass = this.getRowClass.createDelegate(this); view.enableRowBody = true; - grid.on('render', this.onRender, this); grid.on('destroy', this.onDestroy, this); }, // @private onRender: function() { - var grid = this.grid; - var mainBody = grid.getView().mainBody; - mainBody.on('mousedown', this.onMouseDown, this, {delegate: '.x-grid3-row-expander'}); + const + { grid } = this, + { mainBody } = grid.getView() + ; + mainBody.on('mousedown', this.onMouseDown, this, { delegate: '.x-grid3-row-expander' }); if (this.expandOnEnter) { this.keyNav = new Ext.KeyNav(this.grid.getGridEl(), { - 'enter' : this.onEnter, + enter: this.onEnter, scope: this }); } @@ -1884,7 +2386,7 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { // @private onDestroy: function() { - if(this.keyNav){ + if (this.keyNav) { this.keyNav.disable(); delete this.keyNav; } @@ -1893,8 +2395,8 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { * which means the mainBody won't be available. On the off chance that the plugin * isn't destroyed with the grid, take care of removing the listener. */ - var mainBody = this.grid.getView().mainBody; - if(mainBody){ + const { mainBody } = this.grid.getView(); + if (mainBody) { mainBody.un('mousedown', this.onMouseDown, this); } }, @@ -1904,76 +2406,75 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { }, onEnter: function(e) { - var g = this.grid; - var sm = g.getSelectionModel(); - var sels = sm.getSelections(); - for (var i = 0, len = sels.length; i < len; i++) { - var rowIdx = g.getStore().indexOf(sels[i]); + const g = this.grid, + sm = g.getSelectionModel(), + sels = sm.getSelections(); + for (let i = 0, len = sels.length; i < len; i++) { + const rowIdx = g.getStore().indexOf(sels[i]); this.toggleRow(rowIdx); } }, - getBodyContent : function(record, index){ - if(!this.enableCaching){ + getBodyContent: function(record, index) { + if (!this.enableCaching) { return this.tpl.apply(record.data); } - var content = this.bodyContent[record.id]; - if(!content){ + let content = this.bodyContent[record.id]; + if (!content) { content = this.tpl.apply(record.data); this.bodyContent[record.id] = content; } return content; }, - onMouseDown : function(e, t){ + onMouseDown: function(e, t) { e.stopEvent(); - var row = e.getTarget('.x-grid3-row'); + const row = e.getTarget('.x-grid3-row'); this.toggleRow(row); }, - renderer : function(v, p, record){ + renderer: function(v, p, record) { p.cellAttr = 'rowspan="2"'; return '
         
        '; }, - beforeExpand : function(record, body, rowIndex){ - if(this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false){ - if(this.tpl && this.lazyRender){ + beforeExpand: function(record, body, rowIndex) { + if (this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false) { + if (this.tpl && this.lazyRender) { body.innerHTML = this.getBodyContent(record, rowIndex); } return true; - }else{ - return false; } + return false; }, - toggleRow : function(row){ - if(typeof row == 'number'){ + toggleRow: function(row) { + if (typeof row == 'number') { row = this.grid.view.getRow(row); } this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'expandRow' : 'collapseRow'](row); }, - expandRow : function(row){ - if(typeof row == 'number'){ + expandRow: function(row) { + if (typeof row == 'number') { row = this.grid.view.getRow(row); } - var record = this.grid.store.getAt(row.rowIndex); - var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row); - if(this.beforeExpand(record, body, row.rowIndex)){ + const record = this.grid.store.getAt(row.rowIndex), + body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row); + if (this.beforeExpand(record, body, row.rowIndex)) { this.state[record.id] = true; Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded'); this.fireEvent('expand', this, record, body, row.rowIndex); } }, - collapseRow : function(row){ - if(typeof row == 'number'){ + collapseRow: function(row) { + if (typeof row == 'number') { row = this.grid.view.getRow(row); } - var record = this.grid.store.getAt(row.rowIndex); - var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true); - if(this.fireEvent('beforecollapse', this, record, body, row.rowIndex) !== false){ + const record = this.grid.store.getAt(row.rowIndex), + body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true); + if (this.fireEvent('beforecollapse', this, record, body, row.rowIndex) !== false) { this.state[record.id] = false; Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed'); this.fireEvent('collapse', this, record, body, row.rowIndex); @@ -1983,33 +2484,34 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { Ext.preg('rowexpander', Ext.ux.grid.RowExpander); -//backwards compat +// backwards compat Ext.grid.RowExpander = Ext.ux.grid.RowExpander; Ext.ns('Ext.ux.grid'); -Ext.ux.grid.CheckColumn = function (a) { +Ext.ux.grid.CheckColumn = function(a) { Ext.apply(this, a); if (!this.id) { - this.id = Ext.id() + this.id = Ext.id(); } - this.renderer = this.renderer.createDelegate(this) + this.renderer = this.renderer.createDelegate(this); }; Ext.ux.grid.CheckColumn.prototype = { - init: function (b) { + init: function(b) { this.grid = b; - this.grid.on('render', function () { - var a = this.grid.getView(); - a.mainBody.on('mousedown', this.onMouseDown, this) + this.grid.on('render', function() { + const a = this.grid.getView(); + a.mainBody.on('mousedown', this.onMouseDown, this); }, this); - this.grid.on('destroy', this.onDestroy, this) - }, onMouseDown: function (e, t) { + this.grid.on('destroy', this.onDestroy, this); + }, + onMouseDown: function(e, t) { this.grid.fireEvent('rowclick'); - if (t.className && t.className.indexOf('x-grid3-cc-' + this.id) != -1) { + if (t.className && t.className.indexOf(`x-grid3-cc-${this.id}`) !== -1) { e.stopEvent(); - var a = this.grid.getView().findRowIndex(t); - var b = this.grid.store.getAt(a); - var sv = b.data[this.dataIndex]; + const a = this.grid.getView().findRowIndex(t), + b = this.grid.store.getAt(a), + sv = b.data[this.dataIndex]; b.set(this.dataIndex, !sv); this.grid.fireEvent('afteredit', { grid: this.grid, @@ -2020,11 +2522,13 @@ Ext.ux.grid.CheckColumn.prototype = { cancel: false }); } - }, renderer: function (v, p, a) { + }, + renderer: function(v, p, a) { p.css += ' x-grid3-check-col-td'; - return '
         
        ' - }, onDestroy: function () { - var mainBody = this.grid.getView().mainBody; + return `
         
        `; + }, + onDestroy: function() { + const { mainBody } = this.grid.getView(); if (mainBody) { mainBody.un('mousedown', this.onMouseDown, this); } @@ -2033,7 +2537,115 @@ Ext.ux.grid.CheckColumn.prototype = { Ext.preg('checkcolumn', Ext.ux.grid.CheckColumn); Ext.grid.CheckColumn = Ext.ux.grid.CheckColumn; -Ext.grid.PropertyColumnModel=function(a,b){var g=Ext.grid,f=Ext.form;this.grid=a;g.PropertyColumnModel.superclass.constructor.call(this,[{header:this.nameText,width:50,sortable:true,dataIndex:'name',id:'name',menuDisabled:true},{header:this.valueText,width:50,resizable:false,dataIndex:'value',id:'value',menuDisabled:true}]);this.store=b;var c=new f.Field({autoCreate:{tag:'select',children:[{tag:'option',value:'true',html:'true'},{tag:'option',value:'false',html:'false'}]},getValue:function(){return this.el.dom.value=='true'}});this.editors={'date':new g.GridEditor(new f.DateField({selectOnFocus:true})),'string':new g.GridEditor(new f.TextField({selectOnFocus:true})),'number':new g.GridEditor(new f.NumberField({selectOnFocus:true,style:'text-align:left;'})),'boolean':new g.GridEditor(c)};this.renderCellDelegate=this.renderCell.createDelegate(this);this.renderPropDelegate=this.renderProp.createDelegate(this)};Ext.extend(Ext.grid.PropertyColumnModel,Ext.grid.ColumnModel,{nameText:'Name',valueText:'Value',dateFormat:'m/j/Y',renderDate:function(a){return a.dateFormat(this.dateFormat)},renderBool:function(a){return a?'true':'false'},isCellEditable:function(a,b){return a==1},getRenderer:function(a){return a==1?this.renderCellDelegate:this.renderPropDelegate},renderProp:function(v){return this.getPropertyName(v)},renderCell:function(a){var b=a;if(Ext.isDate(a)){b=this.renderDate(a)}else if(typeof a=='boolean'){b=this.renderBool(a)}return Ext.util.Format.htmlEncode(b)},getPropertyName:function(a){var b=this.grid.propertyNames;return b&&b[a]?b[a]:a},getCellEditor:function(a,b){var p=this.store.getProperty(b),n=p.data.name,val=p.data.value;if(this.grid.customEditors[n]){return this.grid.customEditors[n]}if(Ext.isDate(val)){return this.editors.date}else if(typeof val=='number'){return this.editors.number}else if(typeof val=='boolean'){return this.editors['boolean']}else{return this.editors.string}},destroy:function(){Ext.grid.PropertyColumnModel.superclass.destroy.call(this);for(var a in this.editors){Ext.destroy(a)}}}); +Ext.grid.PropertyColumnModel = function(a, b) { + const + g = Ext.grid, + f = Ext.form + ; + this.grid = a; + g.PropertyColumnModel.superclass.constructor.call(this, [ + { + header: this.nameText, + width: 50, + sortable: true, + dataIndex: 'name', + id: 'name', + menuDisabled: true + }, + { + header: this.valueText, + width: 50, + resizable: false, + dataIndex: 'value', + id: 'value', + menuDisabled: true + } + ]); + this.store = b; + const c = new f.Field({ + autoCreate: { + tag: 'select', + children: [ + { tag: 'option', value: 'true', html: 'true' }, + { tag: 'option', value: 'false', html: 'false' } + ] + }, + getValue: function() { + // eslint-disable-next-line eqeqeq + return this.el.dom.value == 'true'; + } + }); + this.editors = { + date: new g.GridEditor(new f.DateField({ selectOnFocus: true })), + string: new g.GridEditor(new f.TextField({ selectOnFocus: true })), + number: new g.GridEditor(new f.NumberField({ selectOnFocus: true, style: 'text-align:left;' })), + boolean: new g.GridEditor(c) + }; + this.renderCellDelegate = this.renderCell.createDelegate(this); + this.renderPropDelegate = this.renderProp.createDelegate(this); +}; +Ext.extend(Ext.grid.PropertyColumnModel, Ext.grid.ColumnModel, { + nameText: 'Name', + valueText: 'Value', + dateFormat: 'm/j/Y', + renderDate: function(a) { + return a.dateFormat(this.dateFormat); + }, + renderBool: function(a) { + return a ? 'true' : 'false'; + }, + isCellEditable: function(a, b) { + // eslint-disable-next-line eqeqeq + return a == 1; + }, + getRenderer: function(a) { + // eslint-disable-next-line eqeqeq + return a == 1 ? this.renderCellDelegate : this.renderPropDelegate; + }, + renderProp: function(v) { + return this.getPropertyName(v); + }, + renderCell: function(a) { + let b = a; + if (Ext.isDate(a)) { + b = this.renderDate(a); + } else if (typeof a == 'boolean') { + b = this.renderBool(a); + } + return Ext.util.Format.htmlEncode(b); + }, + getPropertyName: function(a) { + const b = this.grid.propertyNames; + return b && b[a] ? b[a] : a; + }, + getCellEditor: function(a, b) { + const + p = this.store.getProperty(b), + n = p.data.name, + val = p.data.value + ; + if (this.grid.customEditors[n]) { + return this.grid.customEditors[n]; + } + if (Ext.isDate(val)) { + return this.editors.date; + } + if (typeof val == 'number') { + return this.editors.number; + } + if (typeof val == 'boolean') { + return this.editors.boolean; + } + return this.editors.string; + }, + destroy: function() { + Ext.grid.PropertyColumnModel.superclass.destroy.call(this); + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const a in this.editors) { + Ext.destroy(a); + } + } +}); /** * MODx JSON Grid @@ -2054,18 +2666,17 @@ Ext.grid.PropertyColumnModel=function(a,b){var g=Ext.grid,f=Ext.form;this.grid=a * * [{name: 'key'}, {name: 'value'}] */ -MODx.grid.JsonGrid = function (config) { - config = config || {}; - this.ident = config.ident || 'jsongrid-mecitem' + Ext.id(); +MODx.grid.JsonGrid = function(config = {}) { + this.ident = config.ident || `jsongrid-mecitem${Ext.id()}`; this.hiddenField = new Ext.form.TextArea({ name: config.hiddenName || config.name, hidden: true }); - this.fieldConfig = config.fieldConfig || [{name: 'key'}, {name: 'value'}]; - this.fieldConfig.push({name: 'id', hidden: true}); + this.fieldConfig = config.fieldConfig || [{ name: 'key' }, { name: 'value' }]; + this.fieldConfig.push({ name: 'id', hidden: true }); this.fieldColumns = []; this.fieldNames = []; - Ext.each(this.fieldConfig, function (el) { + Ext.each(this.fieldConfig, function(el) { this.fieldNames.push(el.name); this.fieldColumns.push({ header: el.header || _(el.name), @@ -2084,8 +2695,8 @@ MODx.grid.JsonGrid = function (config) { scope: this }, keyup: { - fn: function (sb) { - var record = this.getSelectionModel().getSelected(); + fn: function(sb) { + const record = this.getSelectionModel().getSelected(); if (record) { record.set(sb.fieldname, sb.el.dom.value); this.saveValue(); @@ -2103,7 +2714,7 @@ MODx.grid.JsonGrid = function (config) { }); }, this); Ext.applyIf(config, { - id: this.ident + '-json-grid', + id: `${this.ident}-json-grid`, fields: this.fieldNames, autoHeight: true, store: new Ext.data.JsonStore({ @@ -2111,12 +2722,12 @@ MODx.grid.JsonGrid = function (config) { data: this.loadValue(config.value) }), enableDragDrop: true, - ddGroup: this.ident + '-json-grid-dd', + ddGroup: `${this.ident}-json-grid-dd`, labelStyle: 'position: absolute', columns: this.fieldColumns, disableContextMenuAction: true, tbar: ['->', { - text: ' ' + _('add'), + text: ` ${_('add')}`, cls: 'primary-button', handler: this.addElement, scope: this @@ -2128,50 +2739,53 @@ MODx.grid.JsonGrid = function (config) { } } }); - MODx.grid.JsonGrid.superclass.constructor.call(this, config) + MODx.grid.JsonGrid.superclass.constructor.call(this, config); }; Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { - getMenu: function () { - var m = []; + getMenu: function() { + const m = []; m.push({ text: _('remove'), handler: this.removeElement }); return m; }, - getActions: function () { + getActions: function() { return [{ action: 'removeElement', icon: 'trash-o', text: _('remove') - }] + }]; }, - addElement: function () { - var ds = this.getStore(); - var row = {}; - Ext.each(this.fieldNames, function (fieldname) { + addElement: function() { + const ds = this.getStore(), + row = {}; + Ext.each(this.fieldNames, function(fieldname) { row[fieldname] = ''; }); - row['id'] = this.getStore().getCount(); + row.id = this.getStore().getCount(); + // eslint-disable-next-line new-cap this.getStore().insert(this.getStore().getCount(), new ds.recordType(row)); this.getView().refresh(); this.getSelectionModel().selectRow(0); }, - removeElement: function () { - Ext.Msg.confirm(_('remove') || '', _('confirm_remove') || '', function (e) { + removeElement: function() { + Ext.Msg.confirm(_('remove') || '', _('confirm_remove') || '', function(e) { if (e === 'yes') { - var ds = this.getStore(); - var rows = this.getSelectionModel().getSelections(); + const ds = this.getStore(), + rows = this.getSelectionModel().getSelections(); if (!rows.length) { return false; } - for (var i = 0; i < rows.length; i++) { - var id = rows[i].id; - var index = ds.findBy(function (record) { - if (record.id === id) { - return true; - } - }); + for (let i = 0; i < rows.length; i++) { + const + { id } = rows[i], + index = ds.findBy(function(record) { + if (record.id === id) { + return true; + } + }) + ; ds.removeAt(index); } this.getView().refresh(); @@ -2179,20 +2793,20 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { } }, this); }, - renderListener: function (grid) { + renderListener: function(grid) { new Ext.dd.DropTarget(grid.container, { copy: false, - ddGroup: this.ident + '-json-grid-dd', - notifyDrop: function (dd, e, data) { - var ds = grid.store; - var sm = grid.getSelectionModel(); - var rows = sm.getSelections(); + ddGroup: `${this.ident}-json-grid-dd`, + notifyDrop: function(dd, e, data) { + const ds = grid.store, + sm = grid.getSelectionModel(), + rows = sm.getSelections(), - var dragData = dd.getDragData(e); + dragData = dd.getDragData(e); if (dragData) { - var cindex = dragData.rowIndex; - if (typeof (cindex) !== "undefined") { - for (var i = 0; i < rows.length; i++) { + const cindex = dragData.rowIndex; + if (typeof (cindex) !== 'undefined') { + for (let i = 0; i < rows.length; i++) { ds.remove(ds.getById(rows[i].id)); } ds.insert(cindex, data.selections); @@ -2206,22 +2820,22 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { this.add(this.hiddenField); this.saveValue(); }, - loadValue: function (value) { + loadValue: function(value) { value = Ext.util.JSON.decode(value); if (value && Array.isArray(value)) { - Ext.each(value, function (record, idx) { - value[idx]['id'] = idx; + Ext.each(value, function(record, idx) { + value[idx].id = idx; }); } else { value = []; } return value; }, - saveValue: function () { - var value = []; - Ext.each(this.getStore().getRange(), function (record) { - var row = {}; - Ext.each(this.fieldNames, function (fieldname) { + saveValue: function() { + const value = []; + Ext.each(this.getStore().getRange(), function(record) { + const row = {}; + Ext.each(this.fieldNames, function(fieldname) { if (fieldname !== 'id') { row[fieldname] = record.data[fieldname]; } @@ -2230,7 +2844,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { }, this); this.hiddenField.setValue(Ext.util.JSON.encode(value)); }, - _getActionsColumnTpl: function () { + _getActionsColumnTpl: function() { return new Ext.XTemplate('' + '' + '
          ' @@ -2243,17 +2857,17 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { compiled: true }); }, - actionsColumnRenderer: function (value, metaData, record, rowIndex, colIndex, store) { + actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { return this._getActionsColumnTpl().apply({ actions: this.getActions() }); }, - onClick: function (e) { - var target = e.getTarget(); - if (!target.classList.contains('x-grid-action')) return; - if (!target.dataset.action) return; + onClick: function(e) { + const target = e.getTarget(); + if (!target.classList.contains('x-grid-action')) { return; } + if (!target.dataset.action) { return; } - var actionHandler = 'action' + target.dataset.action.charAt(0).toUpperCase() + target.dataset.action.slice(1); + let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { actionHandler = target.dataset.action; if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { @@ -2261,8 +2875,8 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { } } - var record = this.getSelectionModel().getSelected(); - var recordIndex = this.store.indexOf(record); + const record = this.getSelectionModel().getSelected(), + recordIndex = this.store.indexOf(record); this.menu.record = record.data; this[actionHandler](record, recordIndex, e); From 9e6179e012a0ae43bcf4a90b0b7cf91831686942 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 31 Oct 2024 17:42:12 -0400 Subject: [PATCH 03/39] ACL Role updates --- core/lexicon/en/user.inc.php | 6 +- .../Processors/Security/Role/GetList.php | 45 +++++---- core/src/Revolution/modUserGroupRole.php | 32 ++++++- .../modext/widgets/security/modx.grid.role.js | 96 +++++++++++++++---- 4 files changed, 139 insertions(+), 40 deletions(-) diff --git a/core/lexicon/en/user.inc.php b/core/lexicon/en/user.inc.php index 984b0326fe..c9904102c6 100644 --- a/core/lexicon/en/user.inc.php +++ b/core/lexicon/en/user.inc.php @@ -41,11 +41,12 @@ $_lang['role_err_ae'] = 'A role already exists with that name.'; $_lang['role_err_duplicate'] = 'An error occurred while duplicating the role.'; $_lang['role_err_has_users'] = 'There are users with this role. It cannot be deleted.'; +$_lang['role_err_name_reserved'] = 'The role name “[[+reservedName]]” is reserved. Please choose another name.'; $_lang['role_err_nf'] = 'Role not found.'; $_lang['role_err_nfs'] = 'Role not found with id: [[+role]]'; $_lang['role_err_ns'] = 'Role not specified!'; $_lang['role_err_ns_authority'] = 'Please specify an authority level for this role.'; -$_lang['role_err_ns_name'] = 'Please specify a name for the role.'; +$_lang['role_err_ns_name'] = 'Please specify a name for this role.'; $_lang['role_err_remove'] = 'An error occurred while trying to delete the role.'; $_lang['role_err_remove_admin'] = 'The role you are trying to delete is the admin role. This role cannot be deleted!'; $_lang['role_remove'] = 'Delete Role'; @@ -75,6 +76,7 @@ $_lang['user_country'] = 'Country'; $_lang['user_dob'] = 'Date of birth'; $_lang['user_doesnt_exist'] = 'User does not exist'; +$_lang['user_edit_account'] = 'Edit User’s Account'; $_lang['user_edit_self_msg'] = 'You may need to log out and log in again after saving to fully update your information.
          Also, should you choose to generate a new password for yourself, it will be sent to you through email.'; $_lang['user_email'] = 'Email address'; $_lang['user_err_access_permissions_save'] = 'An error occurred while saving user access permissions.'; @@ -178,7 +180,7 @@ $_lang['user_remove_confirm'] = 'Are you sure you want to delete this user? This is irreversible!'; $_lang['user_remove_multiple_confirm'] = 'Are you sure you want to delete these users? This is irreversible!'; $_lang['user_remote_data_msg'] = 'Edit remote user data here.'; -$_lang['user_role_update'] = 'Edit User Role'; +$_lang['user_role_update'] = 'Change User’s Role'; $_lang['user_setting_err_remove'] = 'An error occurred while trying to delete user settings.'; $_lang['user_setting_err_save'] = 'An error occurred while saving user settings.'; $_lang['user_settings'] = 'User Settings'; diff --git a/core/src/Revolution/Processors/Security/Role/GetList.php b/core/src/Revolution/Processors/Security/Role/GetList.php index 69693afbed..fc857e393a 100644 --- a/core/src/Revolution/Processors/Security/Role/GetList.php +++ b/core/src/Revolution/Processors/Security/Role/GetList.php @@ -33,7 +33,11 @@ class GetList extends GetListProcessor public $languageTopics = ['user']; public $permission = 'view_role'; public $defaultSortField = 'authority'; + + public $canCreate = false; + public $canEdit = false; public $canRemove = false; + protected $coreRoles; /** * {@inheritDoc} @@ -49,7 +53,10 @@ public function initialize() $this->setProperty('sort', 'name'); } + $this->canCreate = $this->modx->hasPermission('new_role') && $this->modx->hasPermission('save_role'); + $this->canEdit = $this->modx->hasPermission('edit_role') && $this->modx->hasPermission('save_role'); $this->canRemove = $this->modx->hasPermission('delete_role'); + $this->coreRoles = $this->classKey::getCoreRoles(); return $initialized; } @@ -98,30 +105,32 @@ public function isAssigned(int $id) /** * {@inheritDoc} - * @param xPDOObject $object + * @param xPDOObject|modUserGroupRole $object * @return array */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); - $objectId = $object->get('id'); + // Note: Role does not have a checkPolicy() method + $permissions = [ + 'create' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + + $roleData = $object->toArray(); + $roleId = $object->get('id'); $roleName = $object->get('name'); - $isCoreRole = in_array($objectId, [1, 2]) || in_array($roleName, ['Super User', 'Member']); - - $perm = []; - if (!$isCoreRole) { - $perm[] = 'edit'; - if ($this->isAssigned($objectId)) { - $objectArray['isAssigned'] = 1; - } - if ($this->canRemove) { - $perm[] = 'remove'; - } - } else { - $objectArray['isProtected'] = 1; + $isCoreRole = $object->isCoreRole($roleName); + + if ($this->isAssigned($roleId)) { + $roleData['isAssigned'] = 1; } - $objectArray['perm'] = implode(' ', $perm); - return $objectArray; + $roleData['reserved'] = ['name' => $this->coreRoles]; + $roleData['isProtected'] = $isCoreRole; + $roleData['creator'] = $isCoreRole ? 'modx' : strtolower($this->modx->lexicon('user')) ; + $roleData['permissions'] = !$isCoreRole ? $permissions : [] ; + + return $roleData; } } diff --git a/core/src/Revolution/modUserGroupRole.php b/core/src/Revolution/modUserGroupRole.php index b08c5a1031..81d139b5a8 100644 --- a/core/src/Revolution/modUserGroupRole.php +++ b/core/src/Revolution/modUserGroupRole.php @@ -11,13 +11,39 @@ * For example, an Administrator with authority of 1 will automatically inherit any Permissions assigned to a Member * role with authority 9999, since 1 is less than 9999. However, the reverse will not be true. * - * @property string $name The name of the Role + * @property string $name The name of the Role * @property string $description A user-provided description of this Role - * @property int $authority The authority of the role. Lower authority numbers have more power than higher ones, and - * lower numbers will inherit the Permissions of higher numbers. + * @property int $authority The authority of the role. Lower authority numbers + * have more power than higher ones, and lower numbers will inherit + * the Permissions of higher numbers. * * @package MODX\Revolution */ class modUserGroupRole extends xPDOSimpleObject { + public const ROLE_SUPERUSER = 'Super User'; + public const ROLE_MEMBER = 'Member'; + + /** + * Returns a list of core Roles + * + * @return array + */ + public static function getCoreRoles() + { + return [ + self::ROLE_SUPERUSER, + self::ROLE_MEMBER + ]; + } + + /** + * @param string $name The name of the Role + * + * @return bool + */ + public function isCoreRole($name) + { + return in_array($name, static::getCoreRoles(), true); + } } diff --git a/manager/assets/modext/widgets/security/modx.grid.role.js b/manager/assets/modext/widgets/security/modx.grid.role.js index c3323f3a38..db0bc9350c 100644 --- a/manager/assets/modext/widgets/security/modx.grid.role.js +++ b/manager/assets/modext/widgets/security/modx.grid.role.js @@ -20,7 +20,7 @@ MODx.grid.Role = function(config = {}) { 'name', 'description', 'authority', - 'perm' + 'creator' ], paging: true, autosave: true, @@ -33,14 +33,31 @@ MODx.grid.Role = function(config = {}) { }, { header: _('name'), dataIndex: 'name', + id: 'modx-role--name', width: 150, sortable: true, editor: { - xtype: 'textfield' + xtype: 'textfield', + allowBlank: false, + blankText: _('role_err_ns_name'), + validationEvent: 'change', + validator: function(value) { + const + grid = Ext.getCmp('modx-grid-role'), + reserved = this.gridEditor.record.json.reserved.name + ; + if (grid.valueIsReserved(reserved, value)) { + const msg = _('role_err_name_reserved', { reservedName: value }); + Ext.Msg.alert(_('error'), msg); + return false; + } + return true; + } }, renderer: { fn: function(value, metaData, record, rowIndex, colIndex, store) { - metaData.css = this.setEditableCellClasses(record); + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); return Ext.util.Format.htmlEncode(value); }, scope: this @@ -48,19 +65,27 @@ MODx.grid.Role = function(config = {}) { }, { header: _('description'), dataIndex: 'description', + id: 'modx-role--description', width: 350, - editor: { xtype: 'textarea' }, + editor: { + xtype: 'textarea' + }, renderer: { fn: function(value, metaData, record, rowIndex, colIndex, store) { - metaData.css = this.setEditableCellClasses(record); + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); return Ext.util.Format.htmlEncode(value); }, scope: this } - }, { + }, + this.getCreatorColumnConfig('role'), + { header: _('authority'), dataIndex: 'authority', + id: 'modx-role--authority', width: 60, + align: 'center', sortable: true, editor: { xtype: 'numberfield', @@ -72,7 +97,8 @@ MODx.grid.Role = function(config = {}) { }, renderer: { fn: function(value, metaData, record, rowIndex, colIndex, store) { - metaData.css = this.setEditableCellClasses(record, [record.json.isAssigned]); + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isAssigned, record.json.isProtected], '', false); return value; }, scope: this @@ -102,20 +128,51 @@ MODx.grid.Role = function(config = {}) { text: _('create'), cls: 'primary-button', handler: this.createRole, - scope: this - }] + scope: this, + listeners: { + render: { + fn: function(btn) { + if (!this.userCanCreate) { + btn.hide(); + } + }, + scope: this + } + } + }], + viewConfig: this.getViewConfig(false, false) }); MODx.grid.Role.superclass.constructor.call(this, config); - this.on('beforeedit', this.checkCellIsEditable, this); + + this.gridMenuActions = ['delete']; + + this.setUserCanEdit(['save_role', 'edit_role']); + this.setUserCanCreate(['save_role', 'new_role']); + this.setUserCanDelete(['delete_role']); + this.setShowActionsMenu(); + + this.on({ + render: function() { + this.setEditableColumnAccess( + ['modx-role--name', 'modx-role--description', 'modx-role--authority'] + ); + }, + beforeedit: function(e) { + if (e.record.json.isProtected) { + return false; + } + } + }); }; Ext.extend(MODx.grid.Role, MODx.grid.Grid, { + getMenu: function() { const record = this.getSelectionModel().getSelected(), - permissions = record.data.perm || '', + { permissions } = record.json || '', menu = [] ; - if (permissions.indexOf('remove') !== -1) { + if (permissions.delete) { menu.push({ text: _('delete'), handler: this.remove.createDelegate(this, ['role_remove_confirm', 'Security/Role/Remove']) @@ -137,6 +194,7 @@ Ext.extend(MODx.grid.Role, MODx.grid.Grid, { } }); } + }); Ext.reg('modx-grid-role', MODx.grid.Role); @@ -160,17 +218,20 @@ MODx.window.CreateRole = function(config = {}) { fieldLabel: _('name'), xtype: 'textfield' }, { - xtype: MODx.expandHelp ? 'box' : 'hidden', + xtype: 'box', + hidden: !MODx.expandHelp, html: _('role_desc_name'), cls: 'desc-under' }, { name: 'authority', fieldLabel: _('authority'), - xtype: 'textfield', + xtype: 'numberfield', allowNegative: false, - value: 0 + value: 0, + maxValue: 9999 }, { - xtype: MODx.expandHelp ? 'box' : 'hidden', + xtype: 'box', + hidden: !MODx.expandHelp, html: _('role_desc_authority'), cls: 'desc-under' }, { @@ -180,7 +241,8 @@ MODx.window.CreateRole = function(config = {}) { allowBlank: true, grow: true }, { - xtype: MODx.expandHelp ? 'box' : 'hidden', + xtype: 'box', + hidden: !MODx.expandHelp, html: _('role_desc_description'), cls: 'desc-under' }], From ad9be0d8abd6e6b0ff68a45f39e916bc0cc2926e Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 31 Oct 2024 18:19:14 -0400 Subject: [PATCH 04/39] Contexts update Changes to both grid list and Context editing page (general info) --- core/lexicon/en/context.inc.php | 4 + .../src/Revolution/Processors/Context/Get.php | 16 +- .../Revolution/Processors/Context/GetList.php | 58 ++- core/src/Revolution/modContext.php | 34 +- .../widgets/system/modx.grid.context.js | 421 +++++++++++------- .../widgets/system/modx.panel.context.js | 271 ++++++----- 6 files changed, 487 insertions(+), 317 deletions(-) diff --git a/core/lexicon/en/context.inc.php b/core/lexicon/en/context.inc.php index 7352df973c..ca42db57e8 100644 --- a/core/lexicon/en/context.inc.php +++ b/core/lexicon/en/context.inc.php @@ -9,20 +9,24 @@ $_lang['context'] = 'Context'; $_lang['context_add'] = 'Add Context'; $_lang['context_data'] = 'Context Data'; +$_lang['context_edit'] = 'Edit the settings and User Group access for this Context'; $_lang['context_err_ae'] = 'A Context with that name already exists.'; $_lang['context_err_create'] = 'An error occurred while creating the Context.'; $_lang['context_err_duplicate'] = 'An error occurred while trying to duplicate the Context.'; $_lang['context_err_load_data'] = 'Error loading context data.'; +$_lang['context_err_name_reserved'] = 'The context name “[[+reservedName]]” is reserved. Please choose another name.'; $_lang['context_err_nf'] = 'Context not found!'; $_lang['context_err_nfs'] = 'Context not found with key: [[+key]]'; $_lang['context_err_ns'] = 'Context not specified.'; $_lang['context_err_ns_key'] = 'Please specify a valid key for the Context.'; +$_lang['context_err_ns_name'] = 'Please specify a valid name for the Context.'; $_lang['context_err_remove'] = 'An error occurred while trying to delete the Context.'; $_lang['context_err_reserved'] = 'The Context key you chose is reserved for system use only. Please specify a different key.'; $_lang['context_err_save'] = 'An error occurred while saving the Context.'; $_lang['context_id'] = 'Ctx ID'; $_lang['context_key'] = 'Context Key'; $_lang['context_management_message'] = 'Manage site Contexts.'; +$_lang['context_reserved_general_desc'] = 'Note that this is a protected, built-in Context. The values shown below are for informational purposes only. Its settings and assigned User Group(s) are, however, editable by users with the appropriate permissions.'; $_lang['context_settings'] = 'Context Settings'; $_lang['context_settings_desc'] = 'Here you can set settings specific to this Context. Context settings will override any System Settings with the same key. Each setting will be available via the [[++key]] placeholder.'; $_lang['context_with_key_not_found'] = 'Context with key %s not found!'; diff --git a/core/src/Revolution/Processors/Context/Get.php b/core/src/Revolution/Processors/Context/Get.php index 624c8a7708..2f57572987 100644 --- a/core/src/Revolution/Processors/Context/Get.php +++ b/core/src/Revolution/Processors/Context/Get.php @@ -1,4 +1,5 @@ classKey::getCoreContexts(); + $contextKey = $this->object->get('key'); + if (in_array($contextKey, $coreContexts)) { + $contextData = $this->object->toArray(); + $reserved = $contextKey === 'mgr'; + $this->object->set('isProtected', true); + $this->object->set('reserved', $reserved); + } + } +} \ No newline at end of file diff --git a/core/src/Revolution/Processors/Context/GetList.php b/core/src/Revolution/Processors/Context/GetList.php index 56aafadbe5..7e6827b301 100644 --- a/core/src/Revolution/Processors/Context/GetList.php +++ b/core/src/Revolution/Processors/Context/GetList.php @@ -22,8 +22,8 @@ /** * Grabs a list of contexts. * - * @property integer $start (optional) The record to start at. Defaults to 0. - * @property integer $limit (optional) The number of records to limit to. Defaults + * @property int $start (optional) The record to start at. Defaults to 0. + * @property int $limit (optional) The number of records to limit to. Defaults * to 20. * @property string $sort (optional) The column to sort by. Defaults to key. * @property string $dir (optional) The direction of the sort. Defaults to ASC. @@ -36,31 +36,36 @@ class GetList extends GetListProcessor public $permission = 'view_context'; public $languageTopics = ['context']; public $defaultSortField = 'key'; - /** @var boolean $canEdit Determines whether or not the user can edit a Context */ + + /** @var bool $canCreate Determines whether or not the user can create a context (/duplicate one) */ + public $canCreate = false; + /** @var bool $canEdit Determines whether or not the user can edit a Context */ public $canEdit = false; - /** @var boolean $canRemove Determines whether or not the user can remove a Context */ + /** @var bool $canRemove Determines whether or not the user can remove a Context */ public $canRemove = false; - /** @var boolean $canCreate Determines whether or not the user can create a context (/duplicate one) */ - public $canCreate = false; - /** @param boolean $isGridFilter Indicates the target of this list data is a filter field */ + protected $coreContexts; + + /** @var bool $isGridFilter Indicates the target of this list data is a filter field */ protected $isGridFilter = false; /** * {@inheritDoc} - * @return boolean + * @return bool */ public function initialize() { $initialized = parent::initialize(); $this->setDefaultProperties([ 'query' => '', - 'exclude' => '', + 'exclude' => 'creator' ]); $this->canCreate = $this->modx->hasPermission('new_context'); $this->canEdit = $this->modx->hasPermission('edit_context'); $this->canRemove = $this->modx->hasPermission('delete_context'); $this->isGridFilter = $this->getProperty('isGridFilter', false); + $this->coreContexts = $this->classKey::getCoreContexts(); + return $initialized; } @@ -76,7 +81,8 @@ public function prepareQueryBeforeCount(xPDOQuery $c) if (!empty($query)) { $c->where([ 'key:LIKE' => '%' . $query . '%', - 'OR:description:LIKE' => '%' . $query . '%', + 'OR:name:LIKE' => '%' . $query . '%', + 'OR:description:LIKE' => '%' . $query . '%' ]); } $exclude = $this->getProperty('exclude'); @@ -149,24 +155,30 @@ public function prepareQueryAfterCount(xPDOQuery $c) /** * {@inheritDoc} - * @param xPDOObject $object - * + * @param xPDOObject|modContext $object * @return array */ public function prepareRow(xPDOObject $object) { - $contextArray = $object->toArray(); - $contextArray['perm'] = []; - if ($this->canCreate) { - $contextArray['perm'][] = 'pnew'; - } - if ($this->canEdit) { - $contextArray['perm'][] = 'pedit'; - } - if (!in_array($object->get('key'), $this->classKey::RESERVED_KEYS) && $this->canRemove) { - $contextArray['perm'][] = 'premove'; + $permissions = [ + 'create' => $this->canCreate && $object->checkPolicy('save'), + 'duplicate' => $this->canCreate && $object->checkPolicy('copy'), + 'update' => $this->canEdit && $object->checkPolicy('save'), + 'delete' => $this->canRemove && $object->checkPolicy('remove') + ]; + + $contextData = $object->toArray(); + $contextKey = $contextData['key']; + $isCoreContext = $object->isCoreContext($contextKey); + + $contextData['reserved'] = ['key' => $this->coreContexts, 'name' => ['Manager']]; + $contextData['isProtected'] = $isCoreContext; + $contextData['creator'] = $isCoreContext ? 'modx' : strtolower($this->modx->lexicon('user')) ; + if ($isCoreContext) { + unset($permissions['delete']); } + $contextData['permissions'] = $permissions; - return $contextArray; + return $contextData; } } diff --git a/core/src/Revolution/modContext.php b/core/src/Revolution/modContext.php index a86ca5c44e..6454afe109 100644 --- a/core/src/Revolution/modContext.php +++ b/core/src/Revolution/modContext.php @@ -29,6 +29,9 @@ class modContext extends modAccessibleObject * @var array RESERVED_KEYS */ public const RESERVED_KEYS = ['mgr', 'web', 'root']; + public const CONTEXT_MANAGER = 'mgr'; + public const CONTEXT_DEFAULT = 'web'; + public const CONTEXT_DEFAULT_NAME = 'Website'; /** * An array of configuration options for this context @@ -130,7 +133,7 @@ public function prepare($regenerate = false, array $options = []) 'context_settings'), xPDO::OPT_CACHE_HANDLER => $this->xpdo->getOption('cache_context_settings_handler', null, $this->xpdo->getOption(xPDO::OPT_CACHE_HANDLER, null, 'xPDO\Cache\xPDOFileCache')), - xPDO::OPT_CACHE_FORMAT => (integer)$this->xpdo->getOption('cache_context_settings_format', null, + xPDO::OPT_CACHE_FORMAT => (int)$this->xpdo->getOption('cache_context_settings_format', null, $this->xpdo->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)), ]))) { $context = $this->xpdo->cacheManager->generateContext($this->get('key'), $options); @@ -202,9 +205,9 @@ public function findPolicy($context = '') $enabled = true; $context = !empty($context) ? $context : $this->xpdo->context->get('key'); if (!is_object($this->xpdo->context) || $context === $this->xpdo->context->get('key')) { - $enabled = (boolean)$this->xpdo->getOption('access_context_enabled', null, true); + $enabled = (bool)$this->xpdo->getOption('access_context_enabled', null, true); } elseif ($this->xpdo->getContext($context)) { - $enabled = (boolean)$this->xpdo->contexts[$context]->getOption('access_context_enabled', true); + $enabled = (bool)$this->xpdo->contexts[$context]->getOption('access_context_enabled', true); } if ($enabled) { if (empty($this->_policies) || !isset($this->_policies[$context])) { @@ -284,7 +287,7 @@ public function makeUrl($id, $args = '', $scheme = -1, array $options = []) } if ($config['friendly_urls'] == 1) { - if ((integer)$id === (integer)$config['site_start']) { + if ((int)$id === (int)$config['site_start']) { $alias = ($scheme === '' || $scheme === -1) ? $config['base_url'] : ''; $found = true; } else { @@ -480,4 +483,27 @@ public function getResourceURI($id) return $uri; } + + /** + * Returns a list of core Contexts + * + * @return array + */ + public static function getCoreContexts() + { + return [ + self::CONTEXT_MANAGER, + self::CONTEXT_DEFAULT + ]; + } + + /** + * @param string $key The key of the Context + * + * @return bool + */ + public function isCoreContext($key) + { + return in_array($key, static::getCoreContexts(), true); + } } diff --git a/manager/assets/modext/widgets/system/modx.grid.context.js b/manager/assets/modext/widgets/system/modx.grid.context.js index 2532e5bfd7..bf14b497c3 100644 --- a/manager/assets/modext/widgets/system/modx.grid.context.js +++ b/manager/assets/modext/widgets/system/modx.grid.context.js @@ -6,34 +6,37 @@ * @param {Object} config An object of configuration options * @xtype modx-panel-contexts */ -MODx.panel.Contexts = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-contexts' - ,cls: 'container' - ,bodyStyle: '' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('contexts') - ,id: 'modx-contexts-header' - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('contexts') - ,layout: 'form' - ,items: [{ - html: '

          '+_('context_management_message')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-contexts' - ,cls:'main-wrapper' - ,preventRender: true +MODx.panel.Contexts = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-contexts', + cls: 'container', + bodyStyle: '', + defaults: { + collapsible: false, + autoHeight: true + }, + items: [{ + html: _('contexts'), + id: 'modx-contexts-header', + xtype: 'modx-header' + }, MODx.getPageStructure([{ + title: _('contexts'), + layout: 'form', + items: [{ + html: `

          ${_('context_management_message')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-contexts', + urlFilters: ['search'], + cls: 'main-wrapper', + preventRender: true }] }])] }); - MODx.panel.Contexts.superclass.constructor.call(this,config); + MODx.panel.Contexts.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Contexts,MODx.FormPanel); -Ext.reg('modx-panel-contexts',MODx.panel.Contexts); +Ext.extend(MODx.panel.Contexts, MODx.FormPanel); +Ext.reg('modx-panel-contexts', MODx.panel.Contexts); /** * Loads a grid of modContexts. @@ -43,110 +46,188 @@ Ext.reg('modx-panel-contexts',MODx.panel.Contexts); * @param {Object} config An object of configuration properties * @xtype modx-grid-contexts */ -MODx.grid.Context = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('contexts') - ,id: 'modx-grid-context' - ,url: MODx.config.connector_url - ,baseParams: { +MODx.grid.Context = function(config = {}) { + Ext.applyIf(config, { + title: _('contexts'), + id: 'modx-grid-context', + url: MODx.config.connector_url, + baseParams: { action: 'Context/GetList' - } - ,fields: [ + }, + fields: [ 'key', 'name', 'description', - 'perm', - 'rank' - ] - ,paging: true - ,autosave: true - ,save_action: 'Context/UpdateFromGrid' - ,remoteSort: true - ,primaryKey: 'key' - ,columns: [{ - header: _('key') - ,dataIndex: 'key' - ,width: 100 - ,sortable: true - },{ - header: _('name') - ,dataIndex: 'name' - ,width: 150 - ,sortable: true - ,editor: { xtype: 'textfield' } - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=context/update&key=' + record.data.key - }); - }, scope: this } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 575 - ,sortable: false - ,editor: { xtype: 'textarea' } - },{ - header: _('rank') - ,dataIndex: 'rank' - ,width: 100 - ,sortable: true - ,editor: { xtype: 'numberfield' } - }] - ,tbar: [ + 'rank', + 'creator' + ], + paging: true, + autosave: true, + save_action: 'Context/UpdateFromGrid', + remoteSort: true, + primaryKey: 'key', + stateful: true, + stateId: 'modx-grid-context-state', + columns: [{ + header: _('key'), + dataIndex: 'key', + width: 100, + sortable: true + }, { + header: _('name'), + dataIndex: 'name', + id: 'modx-context--name', + width: 150, + sortable: true, + editor: { + xtype: 'textfield', + allowBlank: false, + blankText: _('context_err_ns_name'), + validationEvent: 'change', + validator: function(value) { + const + grid = Ext.getCmp('modx-grid-context'), + reserved = this.gridEditor.record.json.reserved.name + ; + if (grid.valueIsReserved(reserved, value)) { + const msg = _('context_err_name_reserved', { + reservedName: value + }); + Ext.Msg.alert(_('error'), msg); + return false; + } + return true; + } + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected, !(record.json.key === 'web')]); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=context/update&key=${record.data.key}`, + title: _('context_edit') + }) + : value + ; + }, + scope: this + } + }, { + header: _('description'), + dataIndex: 'description', + id: 'modx-context--description', + width: 575, + sortable: false, + editor: { + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected, !(record.json.key === 'web')]); + return value; + }, + scope: this + } + }, + this.getCreatorColumnConfig('context'), + { + header: _('rank'), + dataIndex: 'rank', + id: 'modx-context--rank', + width: 100, + align: 'center', + sortable: true, + editor: { + xtype: 'numberfield' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected, !(record.json.key === 'web')]); + return value; + }, + scope: this + } + }], + tbar: [ { - text: _('create') - ,cls:'primary-button' - ,handler: this.create - ,scope: this + text: _('create'), + cls: 'primary-button', + handler: this.create, + scope: this }, '->', this.getQueryFilterField(), this.getClearFiltersButton() - ] + ], + viewConfig: this.getViewConfig(false) + }); + MODx.grid.Context.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate']; + + this.setUserCanEdit(['save_context', 'edit_context']); + this.setUserCanCreate(['save_context', 'new_context']); + this.setUserCanDelete(['delete_context']); + this.setShowActionsMenu(); + + this.on({ + render: function() { + this.setEditableColumnAccess( + ['modx-context--name', 'modx-context--description', 'modx-context--rank'] + ); + }, + beforeedit: function(e) { + if (e.record.json.key === 'mgr' || !this.userCanEditRecord(e.record)) { + return false; + } + } }); - MODx.grid.Context.superclass.constructor.call(this,config); }; -Ext.extend(MODx.grid.Context,MODx.grid.Grid,{ +Ext.extend(MODx.grid.Context, MODx.grid.Grid, { + getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.perm; - var m = []; - if (p.indexOf('pnew') != -1) { - m.push({ - text: _('duplicate') - ,handler: this.duplicateContext - ,scope: this + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (this.userCanCreate && this.userCanDuplicateRecord(record)) { + menu.push({ + text: _('duplicate'), + handler: this.duplicateContext, + scope: this }); } - - if (p.indexOf('pedit') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateContext + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.updateContext }); } - - if (p.indexOf('premove') != -1) { - m.push('-'); - m.push({ - text: _('delete') - ,handler: this.remove - ,scope: this + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('delete'), + handler: this.remove, + scope: this }); } - return m; - } + return menu; + }, - ,create: function(btn, e) { + create: function(btn, e) { if (this.createWindow) { this.createWindow.destroy(); } this.createWindow = MODx.load({ xtype: 'modx-window-context-create', - closeAction:'close', + closeAction: 'close', listeners: { - 'success': { + success: { fn: function() { this.afterAction(); }, @@ -155,34 +236,39 @@ Ext.extend(MODx.grid.Context,MODx.grid.Grid,{ } }); this.createWindow.show(e.target); - } + }, - ,updateContext: function(itm,e) { - MODx.loadPage('context/update', 'key='+this.menu.record.key); - } + updateContext: function(itm, e) { + MODx.loadPage('context/update', `key=${this.menu.record.key}`); + }, - ,duplicateContext: function() { - var r = { - key: this.menu.record.key - ,newkey: '' - }; - var w = MODx.load({ - xtype: 'modx-window-context-duplicate' - ,record: r - ,listeners: { - 'success': {fn:function() { - this.refresh(); - var tree = Ext.getCmp('modx-resource-tree'); - if (tree) { - tree.refresh(); + duplicateContext: function() { + const + record = { + key: this.menu.record.key, + newkey: '' + }, + window = MODx.load({ + xtype: 'modx-window-context-duplicate', + record: record, + listeners: { + success: { + fn: function() { + this.refresh(); + const tree = Ext.getCmp('modx-resource-tree'); + if (tree) { + tree.refresh(); + } + }, + scope: this } - },scope:this} - } - }); - w.show(); - } + } + }) + ; + window.show(); + }, - ,remove: function(btn, e) { + remove: function(btn, e) { MODx.msg.confirm({ title: _('warning'), text: _('context_remove_confirm'), @@ -192,7 +278,7 @@ Ext.extend(MODx.grid.Context,MODx.grid.Grid,{ key: this.menu.record.key }, listeners: { - 'success': { + success: { fn: function() { this.afterAction(); }, @@ -200,10 +286,10 @@ Ext.extend(MODx.grid.Context,MODx.grid.Grid,{ } } }); - } + }, - ,afterAction: function() { - var cmp = Ext.getCmp('modx-resource-tree'); + afterAction: function() { + const cmp = Ext.getCmp('modx-resource-tree'); if (cmp) { cmp.refresh(); } @@ -211,7 +297,7 @@ Ext.extend(MODx.grid.Context,MODx.grid.Grid,{ this.refresh(); } }); -Ext.reg('modx-grid-contexts',MODx.grid.Context); +Ext.reg('modx-grid-contexts', MODx.grid.Context); /** * Generates the create context window. @@ -221,40 +307,41 @@ Ext.reg('modx-grid-contexts',MODx.grid.Context); * @param {Object} config An object of options. * @xtype modx-window-context-create */ -MODx.window.CreateContext = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('create') - ,url: MODx.config.connector_url - ,action: 'Context/Create' - ,fields: [{ - xtype: 'textfield' - ,fieldLabel: _('context_key') - ,name: 'key' - ,anchor: '100%' - ,maxLength: 100 - },{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'name' - ,anchor: '100%' - ,maxLength: 100 - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,anchor: '100%' - ,grow: true - },{ - xtype: 'numberfield' - ,fieldLabel: _('rank') - ,name: 'rank' - ,allowBlank: true - ,anchor: '100%' - }] - ,keys: [] +MODx.window.CreateContext = function(config = {}) { + Ext.applyIf(config, { + title: _('create'), + url: MODx.config.connector_url, + action: 'Context/Create', + formDefaults: { + anchor: '100%' + }, + fields: [{ + xtype: 'textfield', + fieldLabel: _('context_key'), + name: 'key', + maxLength: 100, + allowBlank: false, + blankText: _('context_err_ns_key') + }, { + xtype: 'textfield', + fieldLabel: _('name'), + name: 'name', + maxLength: 100, + allowBlank: false, + blankText: _('context_err_ns_name') + }, { + xtype: 'textarea', + fieldLabel: _('description'), + name: 'description', + grow: true + }, { + xtype: 'numberfield', + fieldLabel: _('rank'), + name: 'rank' + }], + keys: [] }); - MODx.window.CreateContext.superclass.constructor.call(this,config); + MODx.window.CreateContext.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateContext,MODx.Window); -Ext.reg('modx-window-context-create',MODx.window.CreateContext); +Ext.extend(MODx.window.CreateContext, MODx.Window); +Ext.reg('modx-window-context-create', MODx.window.CreateContext); diff --git a/manager/assets/modext/widgets/system/modx.panel.context.js b/manager/assets/modext/widgets/system/modx.panel.context.js index 9a4270cab0..a838c59aaa 100644 --- a/manager/assets/modext/widgets/system/modx.panel.context.js +++ b/manager/assets/modext/widgets/system/modx.panel.context.js @@ -4,152 +4,181 @@ * @param {Object} config An object of config properties * @xtype modx-panel-context */ -MODx.panel.Context = function(config) { - config = config || {}; - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { +MODx.panel.Context = function(config = {}) { + Ext.applyIf(config, { + url: MODx.config.connector_url, + baseParams: { action: 'Context/Get' - } - ,id: 'modx-panel-context' - ,cls: 'container' - ,class_key: 'modContext' - ,plugin: '' - ,bodyStyle: '' - ,items: [this.getPageHeader(config), MODx.getPageStructure([{ - title: _('general_information') - ,autoHeight: true - ,layout: 'form' - ,defaults: { border: false ,msgTarget: 'side' } - ,items:[{ - xtype: 'panel' - ,border: false - ,cls:'main-wrapper' - ,layout: 'form' - ,items: [{ - xtype: 'statictextfield' - ,fieldLabel: _('key') - ,name: 'key' - ,width: 300 - ,maxLength: 100 - ,enableKeyEvents: true - ,allowBlank: true - ,value: config.context - ,submitValue: true - },{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'name' - ,width: 300 - ,maxLength: 191 - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,width: 300 - ,grow: true - },{ - xtype: 'numberfield' - ,fieldLabel: _('rank') - ,name: 'rank' - ,allowBlank: true - ,width: 300 - },{ - html: MODx.onContextFormRender - ,border: false + }, + id: 'modx-panel-context', + cls: 'container', + class_key: 'modContext', + plugin: '', + bodyStyle: '', + items: [this.getPageHeader(config), MODx.getPageStructure([{ + title: _('general_information'), + autoHeight: true, + layout: 'form', + defaults: { border: false, msgTarget: 'side' }, + items: [{ + xtype: 'modx-description', + id: 'modx-context-general-desc', + hidden: true, + html: '' + }, { + xtype: 'panel', + border: false, + cls: 'main-wrapper', + layout: 'form', + items: [{ + xtype: 'statictextfield', + fieldLabel: _('key'), + name: 'key', + width: 300, + maxLength: 100, + enableKeyEvents: true, + value: config.context, + submitValue: true + }, { + xtype: config.context === 'mgr' ? 'statictextfield' : 'textfield', + fieldLabel: _('name'), + name: 'name', + width: 300, + maxLength: 191 + }, { + xtype: config.context === 'mgr' ? 'statictextarea' : 'textarea', + fieldLabel: _('description'), + name: 'description', + width: 300, + grow: true + }, { + xtype: config.context === 'mgr' ? 'statictextfield' : 'numberfield', + fieldLabel: _('rank'), + name: 'rank', + width: 300 + }, { + html: MODx.onContextFormRender, + border: false }] }] - },{ - title: _('context_settings') - ,autoHeight: true - ,layout: 'form' - ,items: [{ - html: '

          '+_('context_settings_desc')+'

          ' - ,id: 'modx-context-settings-desc' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-context-settings' - ,cls:'main-wrapper' - ,title: '' - ,preventRender: true - ,context_key: config.context - ,listeners: { - 'afteredit': {fn:function() { this.markDirty(); },scope:this} + }, { + title: _('context_settings'), + autoHeight: true, + layout: 'form', + items: [{ + html: `

          ${_('context_settings_desc')}

          `, + id: 'modx-context-settings-desc', + xtype: 'modx-description' + }, { + xtype: 'modx-grid-context-settings', + cls: 'main-wrapper', + title: '', + preventRender: true, + context_key: config.context, + listeners: { + afteredit: { + fn: function() { + this.markDirty(); + }, + scope: this + } } }] - },{ - title: _('access_permissions') - ,autoHeight: true - ,items:[{ - xtype: 'modx-grid-access-context' - ,cls:'main-wrapper' - ,title: '' - ,preventRender: true - ,context_key: config.context - ,listeners: { - 'afteredit': {fn:function() { this.markDirty(); },scope:this} + }, { + title: _('access_permissions'), + autoHeight: true, + items: [{ + xtype: 'modx-grid-access-context', + cls: 'main-wrapper', + title: '', + preventRender: true, + context_key: config.context, + listeners: { + afteredit: { + fn: function() { + this.markDirty(); + }, + scope: this + } } }] - }],{ + }], { id: 'modx-context-tabs' - })] - ,useLoadingMask: true - ,listeners: { - 'setup': {fn:this.setup,scope:this} - ,'success': {fn:this.success,scope:this} - ,'beforeSubmit': {fn:this.beforeSubmit,scope:this} + })], + useLoadingMask: true, + listeners: { + setup: { fn: this.setup, scope: this }, + success: { fn: this.success, scope: this }, + beforeSubmit: { fn: this.beforeSubmit, scope: this } } }); - MODx.panel.Context.superclass.constructor.call(this,config); + MODx.panel.Context.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Context,MODx.FormPanel,{ - initialized: false +Ext.extend(MODx.panel.Context, MODx.FormPanel, { + initialized: false, - ,setup: function() { + setup: function() { if (this.initialized || (this.config.context === '' || this.config.context === 0)) { this.fireEvent('ready'); return false; } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Context/Get' - ,key: this.config.context - } - ,listeners: { - 'success': {fn:function(r) { - this.getForm().setValues(r.object); - Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(r.object.key)); - this.fireEvent('ready'); - MODx.fireEvent('ready'); - this.initialized = true; - },scope:this} + url: this.config.url, + params: { + action: 'Context/Get', + key: this.config.context + }, + listeners: { + success: { + fn: function(response) { + const record = response.object; + this.config.record = record; + if (record.isProtected && record.key !== 'web') { + const descriptionCmp = Ext.getCmp('modx-context-general-desc'); + descriptionCmp.update(_('context_reserved_general_desc')); + descriptionCmp.show(); + } + this.getForm().setValues(record); + Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(record.key)); + this.fireEvent('ready'); + MODx.fireEvent('ready'); + this.initialized = true; + }, + scope: this + } } }); - } - - ,beforeSubmit: function(o) { - var r = {}; - - var g = Ext.getCmp('modx-grid-context-settings'); - if (g) { r.settings = g.encodeModified(); } + }, - Ext.apply(o.form.baseParams,r); - } - - ,success: function(o) { - var g = Ext.getCmp('modx-grid-context-settings'); - if (g) { g.getStore().commitChanges(); } + beforeSubmit: function(o) { + const + data = {}, + settingsCmp = Ext.getCmp('modx-grid-context-settings') + ; + if (settingsCmp) { + data.settings = settingsCmp.encodeModified(); + } + Ext.apply(o.form.baseParams, data); + }, - var t = parent.Ext.getCmp('modx-resource-tree'); - if (t) { t.refresh(); } - } + success: function(o) { + const + settingsCmp = Ext.getCmp('modx-grid-context-settings'), + tree = Ext.getCmp('modx-resource-tree') + ; + if (settingsCmp) { + settingsCmp.getStore().commitChanges(); + } + if (tree) { + tree.refresh(); + } + }, - ,getPageHeader: function(config) { + getPageHeader: function(config) { return MODx.util.getHeaderBreadCrumbs('modx-context-name', [{ text: _('contexts'), href: MODx.getPage('context') }]); } }); -Ext.reg('modx-panel-context',MODx.panel.Context); +Ext.reg('modx-panel-context', MODx.panel.Context); From 8c307ae994d73258b76052ad240bbf4f5343dc4f Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 31 Oct 2024 23:29:41 -0400 Subject: [PATCH 05/39] Media Source updates --- core/lexicon/en/source.inc.php | 3 + .../Revolution/Processors/Source/GetList.php | 56 +- .../Revolution/Processors/Source/Update.php | 26 + .../src/Revolution/Sources/modMediaSource.php | 53 +- .../widgets/source/modx.panel.source.js | 373 +++++++------- .../widgets/source/modx.panel.sources.js | 482 +++++++++--------- .../default/source/update.class.php | 82 ++- 7 files changed, 620 insertions(+), 455 deletions(-) diff --git a/core/lexicon/en/source.inc.php b/core/lexicon/en/source.inc.php index 13a1f4932a..371c0f54de 100644 --- a/core/lexicon/en/source.inc.php +++ b/core/lexicon/en/source.inc.php @@ -20,7 +20,9 @@ $_lang['source_access_remove_confirm'] = 'Are you sure you want to delete Access to this Source for this User Group?'; $_lang['source_access_update'] = 'Edit Access'; $_lang['source_description_desc'] = 'A short description of the Media Source.'; +$_lang['source_edit'] = 'Edit the settings and User Group access for this Media Source'; $_lang['source_err_ae_name'] = 'A Media Source with that name already exists! Please specify a new name.'; +$_lang['source_err_name_reserved'] = 'The source name “[[+reservedName]]” is reserved. Please choose another name.'; $_lang['source_err_nf'] = 'Media Source not found!'; $_lang['source_err_init'] = 'Could not initialize "[[+source]]" Media Source!'; $_lang['source_err_nfs'] = 'No Media Source can be found with the id: [[+id]].'; @@ -30,6 +32,7 @@ $_lang['source_properties.intro_msg'] = 'Manage the properties for this Source below.'; $_lang['source_remove_confirm'] = 'Are you sure you want to delete this Media Source? This might break any TVs you have assigned to this source.'; $_lang['source_remove_multiple_confirm'] = 'Are you sure you want to delete these Media Sources? This might break any TVs you have assigned to these sources.'; +$_lang['source_reserved_general_desc'] = 'Note that this is a protected, built-in Media Source. The values shown below are for informational purposes only. Its properties and assigned User Group(s) are, however, editable by users with the appropriate permissions.'; $_lang['source_type'] = 'Source Type'; $_lang['source_type_desc'] = 'The type, or driver, of the Media Source. The Source will use this driver to connect to when gathering its data. For example: File System will grab files from the file system. S3 will get files from an S3 bucket.'; $_lang['source_type.file'] = 'File System'; diff --git a/core/src/Revolution/Processors/Source/GetList.php b/core/src/Revolution/Processors/Source/GetList.php index 33cd1fbf57..2413aaf111 100644 --- a/core/src/Revolution/Processors/Source/GetList.php +++ b/core/src/Revolution/Processors/Source/GetList.php @@ -33,9 +33,15 @@ class GetList extends GetListProcessor public $languageTopics = ['source']; public $permission = 'source_view'; - /** @param boolean $isGridFilter Indicates the target of this list data is a filter field */ + /** @var bool $isGridFilter Indicates the target of this list data is a filter field */ protected $isGridFilter = false; + public $canCreate = false; + public $canEdit = false; + public $canRemove = false; + + protected $coreSources; + /** * {@inheritDoc} * @return boolean @@ -47,8 +53,15 @@ public function initialize() 'showNone' => false, 'query' => '', 'streamsOnly' => false, + 'exclude' => 'creator' ]); $this->isGridFilter = $this->getProperty('isGridFilter', false); + + $this->canCreate = $this->modx->hasPermission('source_save'); + $this->canEdit = $this->modx->hasPermission('source_edit'); + $this->canRemove = $this->modx->hasPermission('source_delete'); + $this->coreSources = $this->classKey::getCoreSources(); + return $initialized; } @@ -143,31 +156,32 @@ public function getSortClassKey() */ public function prepareRow(xPDOObject $object) { - $canEdit = $this->modx->hasPermission('source_edit'); - $canSave = $this->modx->hasPermission('source_save'); - $canRemove = $this->modx->hasPermission('source_delete'); + $permissions = [ + 'create' => $this->canCreate && $object->checkPolicy('save'), + 'duplicate' => $this->canCreate && $object->checkPolicy('copy'), + 'update' => $this->canEdit && $object->checkPolicy('save'), + 'delete' => $this->canRemove && $object->checkPolicy('remove') + ]; + + $sourceData = $object->toArray(); + $sourceName = $object->get('name'); + $isCoreSource = $object->isCoreSource($sourceName); + + $sourceData['reserved'] = ['name' => $this->coreSources]; + $sourceData['isProtected'] = $isCoreSource; + $sourceData['creator'] = $isCoreSource ? 'modx' : strtolower($this->modx->lexicon('user')) ; + if ($isCoreSource) { + unset($permissions['delete']); + } + $sourceData['permissions'] = $permissions; - $objectArray = $object->toArray(); - $objectArray['iconCls'] = $this->modx->getOption('mgr_source_icon', null, 'icon-folder-open-o'); + $sourceData['iconCls'] = $this->modx->getOption('mgr_source_icon', null, 'icon-folder-open-o'); $props = $object->getPropertyList(); if (isset($props['iconCls']) && !empty($props['iconCls'])) { - $objectArray['iconCls'] = $props['iconCls']; + $sourceData['iconCls'] = $props['iconCls']; } - $cls = []; - if ($canSave && $canEdit && $object->checkPolicy('save')) { - $cls[] = 'pupdate'; - } - if ($canRemove && $object->checkPolicy('remove')) { - $cls[] = 'premove'; - } - if ($canSave && $object->checkPolicy('copy')) { - $cls[] = 'pduplicate'; - } - - $objectArray['cls'] = implode(' ', $cls); - - return $objectArray; + return $sourceData; } } diff --git a/core/src/Revolution/Processors/Source/Update.php b/core/src/Revolution/Processors/Source/Update.php index 0b49c6944d..e206616199 100644 --- a/core/src/Revolution/Processors/Source/Update.php +++ b/core/src/Revolution/Processors/Source/Update.php @@ -1,4 +1,5 @@ object->get('name'); + $id = $this->object->get('id'); + + if (empty($name)) { + $this->addFieldError('name', $this->modx->lexicon('source_err_ns_name')); + } elseif ($this->alreadyExists($name, $id)) { + $this->addFieldError('name', $this->modx->lexicon('source_err_ae_name', [ + 'name' => $name, + ])); + } $this->setSourceProperties(); + return parent::beforeSave(); } + /** + * Check to see if a Media Source with the specified name already exists + * @param string $name + * @return boolean + */ + public function alreadyExists($name, $id) + { + return $this->modx->getCount(modMediaSource::class, [ + 'name' => $name, + 'id:!=' => $id + ]) > 0; + } + /** * Sets the properties on the source * @return void diff --git a/core/src/Revolution/Sources/modMediaSource.php b/core/src/Revolution/Sources/modMediaSource.php index fd01c5fd39..6c5a2c8a0e 100644 --- a/core/src/Revolution/Sources/modMediaSource.php +++ b/core/src/Revolution/Sources/modMediaSource.php @@ -70,6 +70,7 @@ abstract class modMediaSource extends modAccessibleSimpleObject implements modMe /** @var Filesystem */ protected $filesystem; + public const SOURCE_FILESYSTEM = 'Filesystem'; /** * Get the default MODX filesystem source @@ -441,7 +442,6 @@ public function getContainerList($path) foreach ($directories as $dir) { $ls[] = $dir; } - array_multisort($fileNames, SORT_ASC, SORT_STRING, $files); foreach ($files as $file) { $ls[] = $file; @@ -1255,7 +1255,6 @@ public function setVisibility($path, $visibility) return false; } - /** * @param string $object * @@ -1749,10 +1748,26 @@ public function clearCache(array $options = []) $c->select($this->xpdo->escape('key')); $options[xPDO::OPT_CACHE_KEY] = $this->getOption('cache_media_sources_key', $options, 'media_sources'); - $options[xPDO::OPT_CACHE_HANDLER] = $this->getOption('cache_media_sources_handler', $options, $this->getOption(xPDO::OPT_CACHE_HANDLER, $options)); - $options[xPDO::OPT_CACHE_FORMAT] = (int)$this->getOption('cache_media_sources_format', $options, $this->getOption(xPDO::OPT_CACHE_FORMAT, $options, xPDOCacheManager::CACHE_PHP)); - $options[xPDO::OPT_CACHE_ATTEMPTS] = (int)$this->getOption('cache_media_sources_attempts', $options, $this->getOption(xPDO::OPT_CACHE_ATTEMPTS, $options, 10)); - $options[xPDO::OPT_CACHE_ATTEMPT_DELAY] = (int)$this->getOption('cache_media_sources_attempt_delay', $options, $this->getOption(xPDO::OPT_CACHE_ATTEMPT_DELAY, $options, 1000)); + $options[xPDO::OPT_CACHE_HANDLER] = $this->getOption( + 'cache_media_sources_handler', + $options, + $this->getOption(xPDO::OPT_CACHE_HANDLER, $options) + ); + $options[xPDO::OPT_CACHE_FORMAT] = (int)$this->getOption( + 'cache_media_sources_format', + $options, + $this->getOption(xPDO::OPT_CACHE_FORMAT, $options, xPDOCacheManager::CACHE_PHP) + ); + $options[xPDO::OPT_CACHE_ATTEMPTS] = (int)$this->getOption( + 'cache_media_sources_attempts', + $options, + $this->getOption(xPDO::OPT_CACHE_ATTEMPTS, $options, 10) + ); + $options[xPDO::OPT_CACHE_ATTEMPT_DELAY] = (int)$this->getOption( + 'cache_media_sources_attempt_delay', + $options, + $this->getOption(xPDO::OPT_CACHE_ATTEMPT_DELAY, $options, 1000) + ); if ($c->prepare() && $c->stmt->execute()) { while ($row = $c->stmt->fetch(PDO::FETCH_ASSOC)) { @@ -2062,7 +2077,7 @@ protected function getAllowedExtensionsArray($properties = []) /** * @param array $properties * - * @return array|mixed|string + * @return array */ protected function getSkipExtensionsArray($properties = []) { @@ -2073,7 +2088,7 @@ protected function getSkipExtensionsArray($properties = []) $skipExtensions = explode(',', $skipExtensions); } - return !empty($skipExtensions) ? explode(',', $skipExtensions) : []; + return $skipExtensions; } @@ -2450,4 +2465,26 @@ protected function isFileImage($file, $image_extensions = []) return false; } + + /** + * Returns a list of core Media Sources + * + * @return array + */ + public static function getCoreSources() + { + return [ + self::SOURCE_FILESYSTEM + ]; + } + + /** + * @param string $name The name of the Media Source + * + * @return bool + */ + public function isCoreSource($name) + { + return in_array($name, static::getCoreSources(), true); + } } diff --git a/manager/assets/modext/widgets/source/modx.panel.source.js b/manager/assets/modext/widgets/source/modx.panel.source.js index cec222890e..396151653e 100644 --- a/manager/assets/modext/widgets/source/modx.panel.source.js +++ b/manager/assets/modext/widgets/source/modx.panel.source.js @@ -4,223 +4,252 @@ * @param {Object} config An object of configuration properties * @xtype modx-panel-source */ -MODx.panel.Source = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-source' - ,url: MODx.config.connector_url - ,baseParams: { +MODx.panel.Source = function(config = {}) { + let generalIntro = {}; + if (config.record.reserved) { + generalIntro = { + xtype: 'box', + cls: 'panel-desc', + html: _('source_reserved_general_desc') + }; + } + Ext.applyIf(config, { + id: 'modx-panel-source', + url: MODx.config.connector_url, + baseParams: { action: 'Source/Update' - } - ,defaults: { collapsible: false ,autoHeight: true } - ,cls: 'container form-with-labels' - ,items: [this.getPageHeader(config),{ - xtype: 'modx-tabs' - ,defaults: { - autoHeight: true - ,border: true - ,bodyCssClass: 'tab-panel-wrapper' - } - ,id: 'modx-source-tabs' - ,forceLayout: true - ,deferredRender: false - ,stateful: true - ,stateId: 'modx-source-tabpanel' - ,stateEvents: ['tabchange'] - ,getState:function() { - return {activeTab:this.items.indexOf(this.getActiveTab())}; - } - ,items: [{ - title: _('general_information') - ,defaults: { border: false, msgTarget: 'side' } - ,layout: 'form' - ,id: 'modx-source-form' - ,labelWidth: 150 - ,items: [{ - xtype: 'panel' - ,border: false - ,cls: 'main-wrapper' - ,layout: 'form' - ,labelAlign: 'top' - ,items: [{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,anchor: '100%' - ,border: false - } - ,items: [{ - columnWidth: .65 - ,cls: 'main-content' - ,items: [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-source-id' - ,value: config.record.id - },{ - name: 'name' - ,id: 'modx-source-name' - ,xtype: 'textfield' - ,fieldLabel: _('name') - ,description: MODx.expandHelp ? '' : _('source_name_desc') - ,allowBlank: false - ,enableKeyEvents: true - ,anchor: '100%' - ,listeners: { - 'keyup': {scope:this,fn:function(f,e) { - Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getValue())); - }} + }, + defaults: { + collapsible: false, + autoHeight: true + }, + cls: 'container form-with-labels', + items: [this.getPageHeader(config), { + xtype: 'modx-tabs', + defaults: { + autoHeight: true, + border: true, + bodyCssClass: 'tab-panel-wrapper' + }, + id: 'modx-source-tabs', + forceLayout: true, + deferredRender: false, + stateful: true, + stateId: 'modx-source-tabpanel', + stateEvents: ['tabchange'], + getState: function() { + return { + activeTab: this.items.indexOf(this.getActiveTab()) + }; + }, + items: [{ + title: _('general_information'), + layout: 'form', + id: 'modx-source-form', + items: [generalIntro, { + xtype: 'panel', + border: false, + cls: 'main-wrapper', + layout: 'form', + labelAlign: 'top', + items: [{ + layout: 'column', + border: false, + defaults: { + layout: 'form', + labelAlign: 'top', + labelSeparator: '' + }, + items: [{ + columnWidth: 0.65, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + cls: 'main-content', + items: [{ + xtype: 'hidden', + name: 'id', + id: 'modx-source-id', + value: config.record.id + }, { + xtype: config.record.reserved ? 'statictextfield' : 'textfield', + name: 'name', + id: 'modx-source-name', + fieldLabel: _('name'), + description: MODx.expandHelp ? '' : _('source_name_desc'), + allowBlank: false, + enableKeyEvents: true, + listeners: { + keyup: { + scope: this, + fn: function(field, e) { + Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(field.getValue())); + } + } } - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-source-name' - ,html: _('source_name_desc') - ,cls: 'desc-under' - },{ - name: 'description' - ,id: 'modx-source-description' - ,xtype: 'textarea' - ,fieldLabel: _('description') - ,description: MODx.expandHelp ? '' : _('source_description_desc') - ,anchor: '100%' - ,grow: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-source-description' - ,html: _('source_description_desc') - ,cls: 'desc-under' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('source_name_desc'), + cls: 'desc-under' + }, { + xtype: config.record.reserved ? 'statictextarea' : 'textarea', + name: 'description', + id: 'modx-source-description', + fieldLabel: _('description'), + description: MODx.expandHelp ? '' : _('source_description_desc'), + grow: true + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('source_description_desc'), + cls: 'desc-under' }] - },{ - columnWidth: .35 - ,cls: 'main-content' - ,items: [{ - name: 'class_key' - ,hiddenName: 'class_key' - ,id: 'modx-source-type' - ,xtype: 'modx-combo-source-type' - ,fieldLabel: _('source_type') - ,description: MODx.expandHelp ? '' : _('source_type_desc') - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-source-type' - ,html: _('source_type_desc') - ,cls: 'desc-under' + }, { + columnWidth: 0.35, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + cls: 'main-content', + items: [{ + disabled: config.record.reserved, + xtype: 'modx-combo-source-type', + name: 'class_key', + hiddenName: 'class_key', + id: 'modx-source-type', + fieldLabel: _('source_type'), + description: MODx.expandHelp ? '' : _('source_type_desc') + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('source_type_desc'), + cls: 'desc-under' }] }] }] - },{ - html: '

          '+_('source_properties.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-source-properties' - ,preventRender: true - ,source: config.record.id - ,defaultProperties: config.defaultProperties - ,autoHeight: true - ,cls: 'main-wrapper' - ,listeners: { - 'afterRemoveRow': {fn:this.markDirty,scope:this} + }, { + html: `

          ${_('source_properties.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-source-properties', + preventRender: true, + source: config.record.id, + defaultProperties: config.defaultProperties, + autoHeight: true, + cls: 'main-wrapper', + listeners: { + afterRemoveRow: { fn: this.markDirty, scope: this } } }] - },{ - title: _('access') - ,hideMode: 'offsets' - ,items: [{ - html: '

          '+_('source.access.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-source-access' - ,preventRender: true - ,source: config.record.id - ,autoHeight: true - ,cls: 'main-wrapper' - ,listeners: { - 'afterRemoveRow': {fn:this.markDirty,scope:this} - ,'updateRole': {fn:this.markDirty,scope:this} - ,'addMember': {fn:this.markDirty,scope:this} + }, { + title: _('access'), + hideMode: 'offsets', + items: [{ + html: `

          ${_('source.access.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-source-access', + preventRender: true, + source: config.record.id, + autoHeight: true, + cls: 'main-wrapper', + listeners: { + afterRemoveRow: { fn: this.markDirty, scope: this }, + updateRole: { fn: this.markDirty, scope: this }, + addMember: { fn: this.markDirty, scope: this } } }] }] - }] - ,listeners: { - 'setup': {fn:this.setup,scope:this} - ,'success': {fn:this.success,scope:this} - ,'beforeSubmit': {fn:this.beforeSubmit,scope:this} + }], + listeners: { + setup: { fn: this.setup, scope: this }, + success: { fn: this.success, scope: this }, + beforeSubmit: { fn: this.beforeSubmit, scope: this } } }); - MODx.panel.Source.superclass.constructor.call(this,config); + MODx.panel.Source.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Source,MODx.FormPanel,{ - initialized: false +Ext.extend(MODx.panel.Source, MODx.FormPanel, { + initialized: false, - ,setup: function() { - if (this.initialized) { return false; } + setup: function() { + if (this.initialized) { + return false; + } if (Ext.isEmpty(this.config.record.id)) { this.fireEvent('ready'); return false; } + this.getForm().setValues(this.config.record); + /* The component rendering is deferred since we are not using renderTo */ Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(this.config.record.name)); - var g,d; if (!Ext.isEmpty(this.config.record.properties)) { - g = Ext.getCmp('modx-grid-source-properties'); - if (g) { - g.defaultProperties = this.config.defaultProperties; - g.getStore().loadData(this.config.record.properties); + const propsGrid = Ext.getCmp('modx-grid-source-properties'); + if (propsGrid) { + propsGrid.defaultProperties = this.config.defaultProperties; + propsGrid.getStore().loadData(this.config.record.properties); } } if (!Ext.isEmpty(this.config.record.access)) { - d = this.config.record.access; - g = Ext.getCmp('modx-grid-source-access'); - if (g) { - d = Ext.decode(d); - if (!Ext.isEmpty(d)) { - g.defaultProperties = d; - g.getStore().loadData(d); + let { access } = this.config.record; + const accessGrid = Ext.getCmp('modx-grid-source-access'); + if (accessGrid) { + access = Ext.decode(access); + if (!Ext.isEmpty(access)) { + accessGrid.defaultProperties = access; + accessGrid.getStore().loadData(access); } } } - this.fireEvent('ready',this.config.record); + this.fireEvent('ready', this.config.record); MODx.fireEvent('ready'); this.initialized = true; - } + }, - ,beforeSubmit: function(o) { - var bp = {}; - var sp = Ext.getCmp('modx-grid-source-properties'); - if (sp) { - bp.properties = sp.encode(); + beforeSubmit: function(o) { + const + sourceData = {}, + propsGrid = Ext.getCmp('modx-grid-source-properties'), + accessGrid = Ext.getCmp('modx-grid-source-access') + ; + if (propsGrid) { + sourceData.properties = propsGrid.encode(); } - var ap = Ext.getCmp('modx-grid-source-access'); - if (ap) { - bp.access = ap.encode(); + if (accessGrid) { + sourceData.access = accessGrid.encode(); } - Ext.apply(o.form.baseParams,bp); - } + Ext.apply(o.form.baseParams, sourceData); + }, - ,success: function(o) { + success: function(o) { if (Ext.isEmpty(this.config.record) || Ext.isEmpty(this.config.record.id)) { - MODx.loadPage('source/update', 'id='+o.result.object.id); + MODx.loadPage('source/update', `id=${o.result.object.id}`); } else { + const + propsGrid = Ext.getCmp('modx-grid-source-properties'), + accessGrid = Ext.getCmp('modx-grid-source-access') + ; Ext.getCmp('modx-abtn-save').setDisabled(false); - var wg = Ext.getCmp('modx-grid-source-properties'); - if (wg) { wg.getStore().commitChanges(); } - var ag = Ext.getCmp('modx-grid-source-access'); - if (ag) { ag.getStore().commitChanges(); } + if (propsGrid) { + propsGrid.getStore().commitChanges(); + } + if (accessGrid) { + accessGrid.getStore().commitChanges(); + } } - } + }, - ,getPageHeader: function(config) { + getPageHeader: function(config) { return MODx.util.getHeaderBreadCrumbs('modx-source-header', [{ text: _('sources'), href: MODx.getPage('source') }]); } }); -Ext.reg('modx-panel-source',MODx.panel.Source); +Ext.reg('modx-panel-source', MODx.panel.Source); diff --git a/manager/assets/modext/widgets/source/modx.panel.sources.js b/manager/assets/modext/widgets/source/modx.panel.sources.js index 7cce9d4e9a..48e8a59865 100644 --- a/manager/assets/modext/widgets/source/modx.panel.sources.js +++ b/manager/assets/modext/widgets/source/modx.panel.sources.js @@ -6,45 +6,47 @@ * @param {Object} config An object of configuration options * @xtype modx-panel-sources */ -MODx.panel.Sources = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-sources' - ,cls: 'container' - ,bodyStyle: '' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('sources') - ,id: 'modx-sources-header' - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('sources') - ,layout: 'form' - ,items: [{ - html: '

          '+_('sources.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-sources' - ,cls: 'main-wrapper' - ,preventRender: true +MODx.panel.Sources = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-sources', + cls: 'container', + bodyStyle: '', + defaults: { + collapsible: false, + autoHeight: true + }, + items: [{ + html: _('sources'), + id: 'modx-sources-header', + xtype: 'modx-header' + }, MODx.getPageStructure([{ + title: _('sources'), + layout: 'form', + items: [{ + html: `

          ${_('sources.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-sources', + cls: 'main-wrapper', + preventRender: true }] - },{ - layout: 'form' - ,title: _('source_types') - ,items: [{ - html: '

          '+_('source_types.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-source-types' - ,cls: 'main-wrapper' - ,preventRender: true + }, { + layout: 'form', + title: _('source_types'), + items: [{ + html: `

          ${_('source_types.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-source-types', + cls: 'main-wrapper', + preventRender: true }] }])] }); - MODx.panel.Sources.superclass.constructor.call(this,config); + MODx.panel.Sources.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Sources,MODx.FormPanel); -Ext.reg('modx-panel-sources',MODx.panel.Sources); +Ext.extend(MODx.panel.Sources, MODx.FormPanel); +Ext.reg('modx-panel-sources', MODx.panel.Sources); /** * Loads a grid of Sources. @@ -56,168 +58,191 @@ Ext.reg('modx-panel-sources',MODx.panel.Sources); */ MODx.grid.Sources = function(config = {}) { this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { + + Ext.applyIf(config, { + id: 'modx-grid-sources', + url: MODx.config.connector_url, + baseParams: { action: 'Source/GetList' - } - ,fields: [ + }, + fields: [ 'id', 'name', 'description', 'class_key', - 'cls' - ] - ,paging: true - ,autosave: true - ,save_action: 'Source/UpdateFromGrid' - ,remoteSort: true - ,sm: this.sm - ,columns: [this.sm,{ - header: _('id') - ,dataIndex: 'id' - ,width: 50 - ,sortable: true - },{ - header: _('name') - ,dataIndex: 'name' - ,width: 150 - ,sortable: true - ,editor: { xtype: 'textfield' ,allowBlank: false } - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=source/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 300 - ,sortable: false - ,editor: { xtype: 'textarea' } - ,renderer: Ext.util.Format.htmlEncode - }] - ,tbar: [ - { - text: _('create') - ,handler: { - xtype: 'modx-window-source-create', - blankValues: true + 'creator' + ], + paging: true, + autosave: true, + save_action: 'Source/UpdateFromGrid', + remoteSort: true, + sm: this.sm, + stateful: true, + stateId: 'modx-grid-sources-state', + columns: [this.sm, { + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('name'), + dataIndex: 'name', + id: 'modx-source--name', + width: 150, + sortable: true, + editor: { + xtype: 'textfield', + allowBlank: false, + blankText: _('source_err_ns_name'), + validationEvent: 'change', + validator: function(value) { + const grid = Ext.getCmp('modx-grid-sources'), + reserved = this.gridEditor.record.json.reserved.name + ; + if (grid.valueIsReserved(reserved, value)) { + const msg = _('source_err_name_reserved', { reservedName: value }); + Ext.Msg.alert(_('error'), msg); + return false; + } + return true; } - ,cls:'primary-button' - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this - }] }, - '->', - this.getQueryFilterField(), - this.getClearFiltersButton() - ] - }); - MODx.grid.Sources.superclass.constructor.call(this,config); -}; -Ext.extend(MODx.grid.Sources,MODx.grid.Grid,{ - getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; - if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this - }); - } else { - if (p.indexOf('pupdate') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateSource - }); + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=source/update&id=${record.data.id}`, + title: _('source_edit') + }) + : value + ; + }, + scope: this } - if (p.indexOf('pduplicate') != -1) { - m.push({ - text: _('duplicate') - ,handler: this.duplicateSource - }); + }, { + header: _('description'), + dataIndex: 'description', + id: 'modx-source--description', + width: 300, + editor: { + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return value; + }, + scope: this } - if (p.indexOf('premove') != -1 && r.data.id != 1 && r.data.name != 'Filesystem') { - if (m.length > 0) m.push('-'); - m.push({ - text: _('delete') - ,handler: this.removeSource - }); + }, + this.getCreatorColumnConfig('source') + ], + tbar: [{ + text: _('create'), + cls: 'primary-button', + handler: { + xtype: 'modx-window-source-create', + blankValues: true + }, + listeners: { + render: { + fn: function(btn) { + if (!this.userCanCreate) { + btn.hide(); + } + }, + scope: this + } } - } - if (m.length > 0) { - this.addContextMenuItem(m); - } - } + }, + this.getBulkActionsButton('source', 'Source/RemoveMultiple'), + '->', + this.getQueryFilterField(), + this.getClearFiltersButton() + ], + viewConfig: this.getViewConfig() + }); + MODx.grid.Sources.superclass.constructor.call(this, config); - ,createSource: function() { - MODx.loadPage('system/source/create'); - } + this.gridMenuActions = ['edit', 'delete', 'duplicate']; - ,updateSource: function() { - MODx.loadPage('source/update', 'id='+this.menu.record.id); - } + this.setUserCanEdit(['source_save', 'source_edit']); + this.setUserCanCreate(['source_save']); + this.setUserCanDelete(['source_delete']); + this.setShowActionsMenu(); - ,duplicateSource: function(btn,e) { - MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Source/Duplicate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + this.on({ + render: function(grid) { + this.setEditableColumnAccess( + ['modx-source--name', 'modx-source--description'] + ); + }, + beforeedit: function(e) { + if (e.record.json.isProtected || !this.userCanEditRecord(e.record)) { + return false; } - }); - } + } + }); +}; +Ext.extend(MODx.grid.Sources, MODx.grid.Grid, { - ,removeSource: function() { - MODx.msg.confirm({ - title: _('delete') - ,text: _('source_remove_confirm') - ,url: this.config.url - ,params: { - action: 'Source/Remove' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + getMenu: function() { + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.updateSource + }); + } + if (this.userCanCreate && this.userCanDuplicateRecord(record)) { + menu.push({ + text: _('duplicate'), + handler: this.duplicateSource + }); + } + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + if (menu.length > 0) { + menu.push('-'); } - }); - } + menu.push({ + text: _('delete'), + handler: this.confirm.createDelegate(this, ['Source/Remove', 'source_remove_confirm']) + }); + } + return menu; + }, + + createSource: function() { + MODx.loadPage('system/source/create'); + }, - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + updateSource: function() { + MODx.loadPage('source/update', `id=${this.menu.record.id}`); + }, - MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('source_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Source/RemoveMultiple' - ,sources: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + duplicateSource: function(btn, e) { + MODx.Ajax.request({ + url: this.config.url, + params: { + action: 'Source/Duplicate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - return true; } - }); -Ext.reg('modx-grid-sources',MODx.grid.Sources); +Ext.reg('modx-grid-sources', MODx.grid.Sources); /** * Generates the create Source window. @@ -228,68 +253,69 @@ Ext.reg('modx-grid-sources',MODx.grid.Sources); * @xtype modx-window-source-create */ MODx.window.CreateSource = function(config = {}) { - Ext.applyIf(config,{ - title: _('create') - ,url: MODx.config.connector_url - ,autoHeight: true - ,action: 'Source/Create' - ,fields: [{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'name' - ,anchor: '100%' - ,allowBlank: false - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,anchor: '100%' - ,grow: true - },{ - name: 'class_key' - ,hiddenName: 'class_key' - ,xtype: 'modx-combo-source-type' - ,fieldLabel: _('source_type') - ,anchor: '100%' - ,allowBlank: false - ,value: MODx.config.default_media_source_type - }] - ,keys: [] + Ext.applyIf(config, { + title: _('create'), + url: MODx.config.connector_url, + autoHeight: true, + action: 'Source/Create', + formDefaults: { + anchor: '100%', + validationEvent: 'change', + validateOnBlur: false + }, + fields: [{ + xtype: 'textfield', + fieldLabel: _('name'), + name: 'name', + allowBlank: false + }, { + xtype: 'textarea', + fieldLabel: _('description'), + name: 'description', + grow: true + }, { + name: 'class_key', + xtype: 'modx-combo-source-type', + fieldLabel: _('source_type'), + allowBlank: false, + value: MODx.config.default_media_source_type + }], + keys: [] }); - MODx.window.CreateSource.superclass.constructor.call(this,config); + MODx.window.CreateSource.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateSource,MODx.Window); -Ext.reg('modx-window-source-create',MODx.window.CreateSource); +Ext.extend(MODx.window.CreateSource, MODx.Window); +Ext.reg('modx-window-source-create', MODx.window.CreateSource); MODx.grid.SourceTypes = function(config = {}) { - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + url: MODx.config.connector_url, + baseParams: { action: 'Source/Type/GetList' - } - ,fields: [ + }, + fields: [ 'class', 'name', 'description' - ] - ,showActionsColumn: false - ,paging: true - ,remoteSort: true - ,columns: [{ - header: _('name') - ,dataIndex: 'name' - ,width: 150 - ,sortable: true - ,renderer: Ext.util.Format.htmlEncode - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 300 - ,sortable: false - ,renderer: Ext.util.Format.htmlEncode + ], + showActionsColumn: false, + paging: true, + remoteSort: true, + columns: [{ + header: _('name'), + dataIndex: 'name', + width: 150, + sortable: true, + renderer: Ext.util.Format.htmlEncode + }, { + header: _('description'), + dataIndex: 'description', + width: 300, + sortable: false, + renderer: Ext.util.Format.htmlEncode }] }); - MODx.grid.SourceTypes.superclass.constructor.call(this,config); + MODx.grid.SourceTypes.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.SourceTypes,MODx.grid.Grid); -Ext.reg('modx-grid-source-types',MODx.grid.SourceTypes); +Ext.extend(MODx.grid.SourceTypes, MODx.grid.Grid); +Ext.reg('modx-grid-source-types', MODx.grid.SourceTypes); diff --git a/manager/controllers/default/source/update.class.php b/manager/controllers/default/source/update.class.php index 1af8e61cdf..d5ceaaaf6b 100644 --- a/manager/controllers/default/source/update.class.php +++ b/manager/controllers/default/source/update.class.php @@ -1,4 +1,5 @@ modx->hasPermission('source_edit'); } @@ -40,18 +43,28 @@ public function checkPermissions() { * Register custom CSS/JS for the page * @return void */ - public function loadCustomCssJs() { - $mgrUrl = $this->modx->getOption('manager_url',null,MODX_MANAGER_URL); - $this->addJavascript($mgrUrl.'assets/modext/widgets/core/modx.grid.local.property.js'); - $this->addJavascript($mgrUrl.'assets/modext/widgets/source/modx.grid.source.properties.js'); - $this->addJavascript($mgrUrl.'assets/modext/widgets/source/modx.grid.source.access.js'); - $this->addJavascript($mgrUrl.'assets/modext/widgets/source/modx.panel.source.js'); - $this->addJavascript($mgrUrl.'assets/modext/sections/source/update.js'); - $this->addHtml(''); + public function loadCustomCssJs() + { + $mgrUrl = $this->modx->getOption('manager_url', null, MODX_MANAGER_URL); + $this->addJavascript($mgrUrl . 'assets/modext/widgets/core/modx.grid.local.property.js'); + $this->addJavascript($mgrUrl . 'assets/modext/widgets/source/modx.grid.source.properties.js'); + $this->addJavascript($mgrUrl . 'assets/modext/widgets/source/modx.grid.source.access.js'); + $this->addJavascript($mgrUrl . 'assets/modext/widgets/source/modx.panel.source.js'); + $this->addJavascript($mgrUrl . 'assets/modext/sections/source/update.js'); + $record = $this->modx->toJSON($this->sourceArray); + $defaultProps = $this->modx->toJSON($this->sourceDefaultProperties); + $pageCmp = << + Ext.onReady(function() { + MODx.load({ + xtype: 'modx-page-source-update', + record: {$record}, + defaultProperties: {$defaultProps} + }); + }); + +CMP; + $this->addHtml($pageCmp); } /** @@ -59,14 +72,24 @@ public function loadCustomCssJs() { * @param array $scriptProperties * @return mixed */ - public function process(array $scriptProperties = []) { - if (empty($this->scriptProperties['id']) || strlen($this->scriptProperties['id']) !== strlen((integer)$this->scriptProperties['id'])) { + public function process(array $scriptProperties = []) + { + if (empty($this->scriptProperties['id']) || strlen($this->scriptProperties['id']) !== strlen((int)$this->scriptProperties['id'])) { return $this->failure($this->modx->lexicon('source_err_ns')); } $this->source = $this->modx->getObject(modMediaSource::class, ['id' => $this->scriptProperties['id']]); - if (empty($this->source)) return $this->failure($this->modx->lexicon('source_err_nf')); + if (empty($this->source)) { + return $this->failure($this->modx->lexicon('source_err_nf')); + } $this->sourceArray = $this->source->toArray(); + + $coreSources = modMediaSource::getCoreSources(); + $sourceKey = $this->sourceArray['name']; + if (in_array($sourceKey, $coreSources)) { + $this->sourceArray['isProtected'] = true; + $this->sourceArray['reserved'] = true; + } $this->getProperties(); $this->getAccess(); @@ -75,7 +98,8 @@ public function process(array $scriptProperties = []) { return []; } - public function getProperties() { + public function getProperties() + { $properties = $this->source->getProperties(); $data = []; foreach ($properties as $property) { @@ -94,7 +118,8 @@ public function getProperties() { $this->sourceArray['properties'] = $data; } - public function getDefaultProperties() { + public function getDefaultProperties() + { $default = $this->source->getDefaultProperties(); $default = $this->source->prepareProperties($default); $data = []; @@ -115,7 +140,8 @@ public function getDefaultProperties() { return $data; } - public function getAccess() { + public function getAccess() + { $c = $this->modx->newQuery(modAccessMediaSource::class); $c->innerJoin(modMediaSource::class, 'Target'); $c->innerJoin(modAccessPolicy::class, 'Policy'); @@ -131,7 +157,7 @@ public function getAccess() { 'policy_name' => 'Policy.name', 'authority_name' => 'MinimumRole.name', ]); - $acls = $this->modx->getCollection(modAccessMediaSource::class,$c); + $acls = $this->modx->getCollection(modAccessMediaSource::class, $c); $access = []; /** @var modAccessMediaSource $acl */ foreach ($acls as $acl) { @@ -158,15 +184,17 @@ public function getAccess() { * * @return string */ - public function getPageTitle() { - return $this->modx->lexicon('source').': '.$this->sourceArray['name']; + public function getPageTitle() + { + return $this->modx->lexicon('source') . ': ' . $this->sourceArray['name']; } /** * Return the location of the template file * @return string */ - public function getTemplateFile() { + public function getTemplateFile() + { return ''; } @@ -174,7 +202,8 @@ public function getTemplateFile() { * Specify the language topics to load * @return array */ - public function getLanguageTopics() { + public function getLanguageTopics() + { return ['source','namespace','propertyset']; } @@ -182,7 +211,8 @@ public function getLanguageTopics() { * Get the Help URL * @return string */ - public function getHelpUrl() { + public function getHelpUrl() + { return 'Media+Sources'; } } From c7af676cc174bd0538a41da81029020e5c8b0bd8 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 31 Oct 2024 23:38:55 -0400 Subject: [PATCH 06/39] Namespace updates --- core/lexicon/en/namespace.inc.php | 2 +- .../Workspace/PackageNamespace/GetList.php | 95 +++++- core/src/Revolution/modNamespace.php | 64 +++- .../namespace/modx.namespace.panel.js | 311 +++++++++++------- 4 files changed, 325 insertions(+), 147 deletions(-) diff --git a/core/lexicon/en/namespace.inc.php b/core/lexicon/en/namespace.inc.php index e569bf8120..ec549d219d 100644 --- a/core/lexicon/en/namespace.inc.php +++ b/core/lexicon/en/namespace.inc.php @@ -18,7 +18,7 @@ $_lang['namespace_name_desc'] = 'Specify a name for the Namespace here.'; $_lang['namespace_path'] = 'Core Path'; $_lang['namespace_path_desc'] = 'Specify an absolute path to the core for this Namespace here. You may use placeholders like {core_path}. Example: {core_path}components/democomponent/'; -$_lang['namespace_remove_confirm'] = 'Are you sure you want to delete "[[+name]]" namespace and all related content?'; +$_lang['namespace_remove_confirm'] = 'Are you sure you want to delete the "[[+name]]" namespace and all related content?'; $_lang['namespace_remove_multiple_confirm'] = 'Are you sure you want to delete these namespaces and all their related content?'; $_lang['namespaces'] = 'Namespaces'; $_lang['namespaces_desc'] = 'Namespaces are global identifiers for packages and components, registering their vehicles, lexicon entries and resources all together.'; diff --git a/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php b/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php index dd2e7297c6..279318d377 100644 --- a/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php +++ b/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php @@ -14,6 +14,7 @@ use MODX\Revolution\modAccessNamespace; use MODX\Revolution\modNamespace; use MODX\Revolution\modUserGroup; +use MODX\Revolution\Transport\modTransportPackage; use MODX\Revolution\Processors\Model\GetListProcessor; use xPDO\Om\xPDOObject; use xPDO\Om\xPDOQuery; @@ -37,9 +38,16 @@ class GetList extends GetListProcessor public $languageTopics = ['namespace', 'workspace']; public $permission = 'namespaces'; + public $canCreate = false; + public $canEdit = false; + public $canRemove = false; + /** @param boolean $isGridFilter Indicates the target of this list data is a filter field */ protected $isGridFilter = false; + protected $coreNamespaces; + protected $extrasNamespaces = []; + /** * {@inheritDoc} * @return boolean @@ -47,10 +55,27 @@ class GetList extends GetListProcessor public function initialize() { $initialized = parent::initialize(); + $this->isGridFilter = $this->getProperty('isGridFilter', false); $this->setDefaultProperties([ - 'query' => '' + 'query' => '', + 'exclude' => 'creator' ]); - $this->isGridFilter = $this->getProperty('isGridFilter', false); + + /* + Normally these would access permission like this: + $this->canCreate = $this->modx->hasPermission('[object type]_save'); + Namespaces do not currently have changeable policy permissions, so + setting each to true; consider adding new permissions settings for + - namespace_save + - namespace_edit + - namespace_delete + */ + $this->canCreate = $this->modx->hasPermission('namespaces'); + $this->canEdit = $this->modx->hasPermission('namespaces'); + $this->canRemove = $this->modx->hasPermission('namespaces'); + $this->coreNamespaces = $this->classKey::getCoreNamespaces(); + $this->extrasNamespaces = $this->getExtrasNamespaces(); + return $initialized; } @@ -168,18 +193,72 @@ public function prepareQueryAfterCount(xPDOQuery $c) return $c; } + public function getExtrasNamespaces() + { + $namespaceList = []; + + $c = $this->modx->newQuery(modTransportPackage::class); + $c->select([ + 'name' => 'DISTINCT SUBSTRING_INDEX(`signature`,"-",1)' + ]); + $namespaces = $this->modx->getIterator(modTransportPackage::class, $c); + $namespaces->rewind(); + if ($namespaces->valid()) { + foreach ($namespaces as $namespace) { + $namespaceList[] = $namespace->get('name'); + } + } + return $namespaceList; + } + /** * Prepare the Namespace for listing - * @param xPDOObject $object + * @param xPDOObject|modNamespace $object * @return array */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); - $objectArray['perm'] = []; - $objectArray['perm'][] = 'pedit'; - $objectArray['perm'][] = 'premove'; + /* + If policy permissions get added for namespaces, change to: + $permissions = [ + 'create' => $this->canCreate && $object->checkPolicy('save'), + 'duplicate' => $this->canCreate && $object->checkPolicy('copy'), + 'update' => $this->canEdit && $object->checkPolicy('save'), + 'delete' => $this->canRemove && $object->checkPolicy('remove') + ]; + */ + $permissions = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + + $namespaceData = $object->toArray(); + $namespaceName = $object->get('name'); + $isCoreNamespace = $object->isCoreNamespace($namespaceName); + + $namespaceData['reserved'] = ['name' => $this->coreNamespaces]; + $namespaceData['isProtected'] = true; + $namespaceData['isExtrasNamespace'] = in_array($namespaceName, $this->extrasNamespaces); + + switch (true) { + case $namespaceData['isExtrasNamespace']: + $namespaceData['creator'] = 'extra'; + break; + case $isCoreNamespace: + $namespaceData['creator'] = 'modx'; + break; + default: + $namespaceData['creator'] = 'user'; + $namespaceData['isProtected'] = false; + } + // Core and Extras paths should only be editable via the installation process + if ($isCoreNamespace || $namespaceData['isExtrasNamespace']) { + $permissions = []; + } + $namespaceData['permissions'] = $permissions; - return $objectArray; + return $namespaceData; } } diff --git a/core/src/Revolution/modNamespace.php b/core/src/Revolution/modNamespace.php index 7c06ba8a16..d5d973d5fd 100644 --- a/core/src/Revolution/modNamespace.php +++ b/core/src/Revolution/modNamespace.php @@ -27,6 +27,8 @@ */ class modNamespace extends modAccessibleObject { + public const NAMESPACE_CORE = 'core'; + public function save($cacheFlag = null) { $saved = parent::save(); @@ -54,11 +56,21 @@ public static function loadCache(modX $modx) } $cacheKey = 'namespaces'; $cache = $modx->cacheManager->get($cacheKey, [ - xPDO::OPT_CACHE_KEY => $modx->getOption('cache_namespaces_key', null, 'namespaces'), - xPDO::OPT_CACHE_HANDLER => $modx->getOption('cache_namespaces_handler', null, - $modx->getOption(xPDO::OPT_CACHE_HANDLER)), - xPDO::OPT_CACHE_FORMAT => (integer)$modx->getOption('cache_namespaces_format', null, - $modx->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)), + xPDO::OPT_CACHE_KEY => $modx->getOption( + 'cache_namespaces_key', + null, + 'namespaces' + ), + xPDO::OPT_CACHE_HANDLER => $modx->getOption( + 'cache_namespaces_handler', + null, + $modx->getOption(xPDO::OPT_CACHE_HANDLER) + ), + xPDO::OPT_CACHE_FORMAT => (int)$modx->getOption( + 'cache_namespaces_format', + null, + $modx->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP) + ) ]); if (empty($cache)) { $cache = $modx->cacheManager->generateNamespacesCache($cacheKey); @@ -71,11 +83,21 @@ public static function clearCache(modX $modx) { $cacheKey = 'namespaces'; $cleared = $modx->cacheManager->delete($cacheKey, [ - xPDO::OPT_CACHE_KEY => $modx->getOption('cache_namespaces_key', null, 'namespaces'), - xPDO::OPT_CACHE_HANDLER => $modx->getOption('cache_namespaces_handler', null, - $modx->getOption(xPDO::OPT_CACHE_HANDLER)), - xPDO::OPT_CACHE_FORMAT => (integer)$modx->getOption('cache_namespaces_format', null, - $modx->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)), + xPDO::OPT_CACHE_KEY => $modx->getOption( + 'cache_namespaces_key', + null, + 'namespaces' + ), + xPDO::OPT_CACHE_HANDLER => $modx->getOption( + 'cache_namespaces_handler', + null, + $modx->getOption(xPDO::OPT_CACHE_HANDLER) + ), + xPDO::OPT_CACHE_FORMAT => (int)$modx->getOption( + 'cache_namespaces_format', + null, + $modx->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP) + ) ]); return $cleared; @@ -157,4 +179,26 @@ public function findPolicy($context = '') return $policy; } + + /** + * Returns a list of core Namespaces + * + * @return array + */ + public static function getCoreNamespaces() + { + return [ + self::NAMESPACE_CORE + ]; + } + + /** + * @param string $key The key of the Context + * + * @return bool + */ + public function isCoreNamespace($key) + { + return in_array($key, static::getCoreNamespaces(), true); + } } diff --git a/manager/assets/modext/workspace/namespace/modx.namespace.panel.js b/manager/assets/modext/workspace/namespace/modx.namespace.panel.js index 90aaaba74e..d92bbcae31 100644 --- a/manager/assets/modext/workspace/namespace/modx.namespace.panel.js +++ b/manager/assets/modext/workspace/namespace/modx.namespace.panel.js @@ -6,34 +6,41 @@ * @param {Object} config An object of configuration properties * @xtype modx-panel-namespaces */ -MODx.panel.Namespaces = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-namespaces' - ,cls: 'container' - ,bodyStyle: '' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('namespaces') - ,id: 'modx-namespaces-header' - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('namespaces') - ,layout: 'form' - ,items: [{ - html: '

          '+_('namespaces_desc')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-namespace' - ,cls:'main-wrapper' - ,preventRender: true - }] - }])] +MODx.panel.Namespaces = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-namespaces', + cls: 'container', + bodyStyle: '', + defaults: { + collapsible: false, + autoHeight: true + }, + items: [ + { + html: _('namespaces'), + id: 'modx-namespaces-header', + xtype: 'modx-header' + }, + MODx.getPageStructure([{ + title: _('namespaces'), + layout: 'form', + items: [ + { + html: `

          ${_('namespaces_desc')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-namespace', + cls: 'main-wrapper', + preventRender: true + } + ] + }]) + ] }); - MODx.panel.Namespaces.superclass.constructor.call(this,config); + MODx.panel.Namespaces.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Namespaces,MODx.FormPanel); -Ext.reg('modx-panel-namespaces',MODx.panel.Namespaces); +Ext.extend(MODx.panel.Namespaces, MODx.FormPanel); +Ext.reg('modx-panel-namespaces', MODx.panel.Namespaces); /** * Loads a grid for managing namespaces. @@ -43,121 +50,169 @@ Ext.reg('modx-panel-namespaces',MODx.panel.Namespaces); * @param {Object} config An object of configuration properties * @xtype modx-grid-namespace */ -MODx.grid.Namespace = function(config) { - config = config || {}; +MODx.grid.Namespace = function(config = {}) { this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + id: 'modx-grid-namespaces', + url: MODx.config.connector_url, + baseParams: { action: 'Workspace/PackageNamespace/GetList' - } - ,fields: [ - 'id', + }, + fields: [ 'name', 'path', 'assets_path', - 'perm' - ] - ,anchor: '100%' - ,paging: true - ,autosave: true - ,save_action: 'Workspace/PackageNamespace/UpdateFromGrid' - ,primaryKey: 'name' - ,remoteSort: true - ,sm: this.sm - ,columns: [this.sm,{ - header: _('name') - ,dataIndex: 'name' - ,width: 200 - ,sortable: true - },{ - header: _('namespace_path') - ,dataIndex: 'path' - ,width: 500 - ,sortable: false - ,editor: { xtype: 'textfield' } - },{ - header: _('namespace_assets_path') - ,dataIndex: 'assets_path' - ,width: 500 - ,sortable: false - ,editor: { xtype: 'textfield' } - }] - ,tbar: [ - { - text: _('create') - ,handler: { xtype: 'modx-window-namespace-create' ,blankValues: true } - ,cls:'primary-button' - ,scope: this + 'perm', + 'creator' + ], + anchor: '100%', + paging: true, + autosave: true, + save_action: 'Workspace/PackageNamespace/UpdateFromGrid', + primaryKey: 'name', + remoteSort: true, + sm: this.sm, + columns: [this.sm, { + header: _('name'), + dataIndex: 'name', + id: 'modx-namespace--name', + width: 200, + sortable: true, + // because PK is name, allowing edit is tricky as implemented; leave for now + listeners: { + click: { + fn: function(column, grid, rowIndex, e) { + if (e.target.classList.contains('simulated-link')) { + this.updateNamespace(e); + } + }, + scope: this + } + } + }, { + header: _('namespace_path'), + dataIndex: 'path', + id: 'modx-namespace--path', + width: 500, + sortable: false, + editor: { + xtype: 'textfield' }, - '->', - this.getQueryFilterField(), - this.getClearFiltersButton() - ] + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses( + record, + [record.json.isProtected, record.json.isExtrasNamespace], + '', + false + ); + return value; + }, + scope: this + } + }, { + header: _('namespace_assets_path'), + dataIndex: 'assets_path', + id: 'modx-namespace--assets_path', + width: 500, + sortable: false, + editor: { + xtype: 'textfield' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses( + record, + [record.json.isProtected, record.json.isExtrasNamespace], + '', + false + ); + return value; + }, + scope: this + } + }, + this.getCreatorColumnConfig('namespace') + ], + tbar: [{ + text: _('create'), + handler: { + xtype: 'modx-window-namespace-create', + blankValues: true + }, + cls: 'primary-button', + scope: this + }, + this.getBulkActionsButton('namespace', 'Workspace/PackageNamespace/RemoveMultiple', 'string'), + '->', + this.getQueryFilterField(), + this.getClearFiltersButton() + ], + viewConfig: this.getViewConfig() + }); + MODx.grid.Namespace.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete']; + + // Note there are currently no action-specific permissions for Namespaces + this.setUserCanEdit(['namespaces']); + this.setUserCanCreate(['namespaces']); + this.setUserCanDelete(['namespaces']); + this.setShowActionsMenu(); + + this.on({ + render: function() { + this.setEditableColumnAccess( + ['modx-namespace--path', 'modx-namespace--assets_path'] + ); + }, + beforeedit: function(e) { + return !(e.record.json.isProtected || e.record.json.isExtrasNamespace); + } }); - MODx.grid.Namespace.superclass.constructor.call(this,config); }; -Ext.extend(MODx.grid.Namespace,MODx.grid.Grid,{ +Ext.extend(MODx.grid.Namespace, MODx.grid.Grid, { + getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.perm; - var m = []; - if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + const record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.updateNamespace }); - } else { - m.push({ - text: _('edit') - ,handler: this.namespaceUpdate - }); - if (p.indexOf('premove') != -1 && this.menu.record.name != 'core') { - m.push({ - text: _('delete') - ,handler: this.remove.createDelegate(this,['namespace_remove_confirm','Workspace/PackageNamespace/Remove']) - }); + } + if (this.userCanDelete && !record.json.isProtected) { + if (menu.length > 0) { + menu.push('-'); } + menu.push({ + text: _('delete'), + handler: this.remove.createDelegate(this, ['namespace_remove_confirm', 'Workspace/PackageNamespace/Remove']) + }); } - return m; - } + return menu; + }, - ,namespaceUpdate: function(elem, vent) { - var win = MODx.load({ - xtype: 'modx-window-namespace-update' - ,record: this.menu.record - ,listeners: { - success: { - fn: this.refresh - ,scope: this + updateNamespace: function(e) { + const + record = this.getSelectionModel().getSelected().data, + window = MODx.load({ + xtype: 'modx-window-namespace-update', + record: record, + listeners: { + success: { + fn: this.refresh, + scope: this + } } - } - }); - win.setValues(this.menu.record); - win.show(vent.target); - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; - - MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('namespace_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Workspace/PackageNamespace/RemoveMultiple' - ,namespaces: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} - } - }); - return true; + }) + ; + window.setValues(record); + window.show(e.target); } }); -Ext.reg('modx-grid-namespace',MODx.grid.Namespace); +Ext.reg('modx-grid-namespace', MODx.grid.Namespace); From e7cd4467d2d97ed9ea26e0db1e2643c2e2b55dc8 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 1 Nov 2024 14:34:32 -0400 Subject: [PATCH 07/39] Dashboard panel code formatting updates --- .../Processors/System/Dashboard/Update.php | 14 +- .../widgets/system/modx.panel.dashboard.js | 730 ++++++++---------- .../system/dashboards/update.class.php | 44 +- 3 files changed, 366 insertions(+), 422 deletions(-) diff --git a/core/src/Revolution/Processors/System/Dashboard/Update.php b/core/src/Revolution/Processors/System/Dashboard/Update.php index baa6202133..aed74a45ea 100644 --- a/core/src/Revolution/Processors/System/Dashboard/Update.php +++ b/core/src/Revolution/Processors/System/Dashboard/Update.php @@ -1,4 +1,5 @@ modx->getCollection(modDashboardWidgetPlacement::class, ['dashboard' => $this->object->id, 'user' => 0]); - $previousWidgets = array_map(function($item){ + $previousWidgets = $this->modx->getCollection(modDashboardWidgetPlacement::class, [ + 'dashboard' => $this->object->id, + 'user' => 0 + ]); + $previousWidgets = array_map(function ($item) { return $item->widget; }, $previousWidgets); @@ -63,7 +67,6 @@ public function setWidgets() ]); foreach ($widgets as $data) { $newWidgets[] = $data['widget']; - $key = [ 'dashboard' => $this->object->get('id'), 'user' => 0, @@ -79,7 +82,6 @@ public function setWidgets() $widget->set('rank', $data['rank']); $widget->save(); } - $addedWidgets = array_values(array_diff($newWidgets, $previousWidgets)); $removedWidgets = array_values(array_diff($previousWidgets, $newWidgets)); @@ -88,9 +90,7 @@ public function setWidgets() $userDashboardsQuery->distinct(true); $userDashboardsQuery->select('user'); $userDashboardsQuery->prepare(); - $userDashboardsQuery->stmt->execute(); - $userDashboards = $userDashboardsQuery->stmt->fetchAll(\PDO::FETCH_COLUMN, 0); $userDashboards = array_map('intval', $userDashboards); @@ -108,11 +108,9 @@ public function setWidgets() } } } - if (!empty($removedWidgets)) { $this->modx->removeCollection(modDashboardWidgetPlacement::class, ['dashboard' => $this->object->id, 'widget:IN' => $removedWidgets]); } - $this->object->sortWidgets(); } } diff --git a/manager/assets/modext/widgets/system/modx.panel.dashboard.js b/manager/assets/modext/widgets/system/modx.panel.dashboard.js index 63e60af281..9fdb351107 100644 --- a/manager/assets/modext/widgets/system/modx.panel.dashboard.js +++ b/manager/assets/modext/widgets/system/modx.panel.dashboard.js @@ -4,148 +4,150 @@ * @param {Object} config An object of configuration properties * @xtype modx-panel-dashboard */ -MODx.panel.Dashboard = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-dashboard' - ,url: MODx.config.connector_url - ,baseParams: { +MODx.panel.Dashboard = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-dashboard', + url: MODx.config.connector_url, + baseParams: { action: 'System/Dashboard/Update' - } - ,cls: 'container' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [this.getPageHeader(config),{ - xtype: 'modx-tabs' - ,defaults: { - autoHeight: true - ,border: false - } - ,id: 'modx-dashboard-tabs' - ,forceLayout: true - ,deferredRender: false - ,stateful: true - ,stateId: 'modx-dashboard-tabpanel' - ,stateEvents: ['tabchange'] - ,getState:function() { - return {activeTab:this.items.indexOf(this.getActiveTab())}; - } + }, + cls: 'container', + defaults: { collapsible: false, autoHeight: true }, + items: [this.getPageHeader(config), { + xtype: 'modx-tabs', + defaults: { + autoHeight: true, + border: false + }, + id: 'modx-dashboard-tabs', + forceLayout: true, + deferredRender: false, + stateful: true, + stateId: 'modx-dashboard-tabpanel', + stateEvents: ['tabchange'], + getState: function() { + return { activeTab: this.items.indexOf(this.getActiveTab()) }; + }, // todo: the layout is inconsistent with other panels, refactor the structure - ,items: [{ - title: _('general_information') - ,cls: 'form-with-labels' - ,defaults: { border: false, cls: 'main-wrapper' } - ,layout: 'form' - ,id: 'modx-dashboard-form' - ,labelAlign: 'top' - ,items: [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-dashboard-id' - ,value: config.record.id - },{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,anchor: '100%' - ,border: false - } - ,items: [{ - columnWidth: .7 - ,cls: 'main-content' - ,items: [{ - name: 'name' - ,id: 'modx-dashboard-name' - ,xtype: 'textfield' - ,fieldLabel: _('name') - ,description: MODx.expandHelp ? '' : _('dashboard_desc_name') - ,allowBlank: false - ,enableKeyEvents: true - ,anchor: '100%' - ,listeners: { - 'keyup': {scope:this,fn:function(f,e) { - Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getValue())); - }} + items: [{ + title: _('general_information'), + cls: 'form-with-labels', + defaults: { border: false, cls: 'main-wrapper' }, + layout: 'form', + id: 'modx-dashboard-form', + labelAlign: 'top', + items: [{ + xtype: 'hidden', + name: 'id', + id: 'modx-dashboard-id', + value: config.record.id + }, { + layout: 'column', + border: false, + defaults: { + layout: 'form', + labelAlign: 'top', + anchor: '100%', + border: false + }, + items: [{ + columnWidth: 0.7, + cls: 'main-content', + items: [{ + name: 'name', + id: 'modx-dashboard-name', + xtype: 'textfield', + fieldLabel: _('name'), + description: MODx.expandHelp ? '' : _('dashboard_desc_name'), + allowBlank: false, + enableKeyEvents: true, + anchor: '100%', + listeners: { + keyup: { + fn: function(f, e) { + Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getValue())); + }, + scope: this + } } - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-dashboard-name' - ,html: _('dashboard_desc_name') - ,cls: 'desc-under' - },{ - name: 'description' - ,id: 'modx-dashboard-description' - ,xtype: 'textarea' - ,fieldLabel: _('description') - ,description: MODx.expandHelp ? '' : _('dashboard_desc_description') - ,anchor: '100%' - ,grow: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-dashboard-description' - ,html: _('dashboard_desc_description') - ,cls: 'desc-under' + }, { + xtype: MODx.expandHelp ? 'label' : 'hidden', + forId: 'modx-dashboard-name', + html: _('dashboard_desc_name'), + cls: 'desc-under' + }, { + name: 'description', + id: 'modx-dashboard-description', + xtype: 'textarea', + fieldLabel: _('description'), + description: MODx.expandHelp ? '' : _('dashboard_desc_description'), + anchor: '100%', + grow: true + }, { + xtype: MODx.expandHelp ? 'label' : 'hidden', + forId: 'modx-dashboard-description', + html: _('dashboard_desc_description'), + cls: 'desc-under' }] - },{ - columnWidth: .3 - ,cls: 'main-content' - ,items: [{ - name: 'hide_trees' - ,id: 'modx-dashboard-hide-trees' - ,xtype: 'xcheckbox' - ,boxLabel: _('dashboard_hide_trees') - ,description: MODx.expandHelp ? '' : _('dashboard_desc_hide_trees') - ,inputValue: 1 - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-dashboard-hide-trees' - ,html: _('dashboard_desc_hide_trees') - ,cls: 'desc-under' - },{ - name: 'customizable' - ,id: 'modx-dashboard-customizable' - ,xtype: 'xcheckbox' - ,boxLabel: _('dashboard_customizable') - ,description: MODx.expandHelp ? '' : _('dashboard_desc_customizable') - ,inputValue: 1 - ,checked: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-dashboard-customizable' - ,html: _('dashboard_desc_customizable') - ,cls: 'desc-under' + }, { + columnWidth: 0.3, + cls: 'main-content', + items: [{ + name: 'hide_trees', + id: 'modx-dashboard-hide-trees', + xtype: 'xcheckbox', + boxLabel: _('dashboard_hide_trees'), + description: MODx.expandHelp ? '' : _('dashboard_desc_hide_trees'), + inputValue: 1 + }, { + xtype: MODx.expandHelp ? 'label' : 'hidden', + forId: 'modx-dashboard-hide-trees', + html: _('dashboard_desc_hide_trees'), + cls: 'desc-under' + }, { + name: 'customizable', + id: 'modx-dashboard-customizable', + xtype: 'xcheckbox', + boxLabel: _('dashboard_customizable'), + description: MODx.expandHelp ? '' : _('dashboard_desc_customizable'), + inputValue: 1, + checked: true + }, { + xtype: MODx.expandHelp ? 'label' : 'hidden', + forId: 'modx-dashboard-customizable', + html: _('dashboard_desc_customizable'), + cls: 'desc-under' }] }] - },{ - html: '

          '+_('dashboard_widgets.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-dashboard-widget-placements' - ,preventRender: true - ,dashboard: config.record.id - ,autoHeight: true - ,anchor: '100%' - ,listeners: { - 'afterRemoveRow': {fn:this.markDirty,scope:this} - ,'updateRole': {fn:this.markDirty,scope:this} - ,'addMember': {fn:this.markDirty,scope:this} + }, { + html: `

          ${_('dashboard_widgets.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-dashboard-widget-placements', + preventRender: true, + dashboard: config.record.id, + autoHeight: true, + anchor: '100%', + listeners: { + afterRemoveRow: { fn: this.markDirty, scope: this }, + updateRole: { fn: this.markDirty, scope: this }, + addMember: { fn: this.markDirty, scope: this } } }] }] - }] - ,listeners: { - 'setup': {fn:this.setup,scope:this} - ,'success': {fn:this.success,scope:this} - ,'beforeSubmit': {fn:this.beforeSubmit,scope:this} + }], + listeners: { + setup: { fn: this.setup, scope: this }, + success: { fn: this.success, scope: this }, + beforeSubmit: { fn: this.beforeSubmit, scope: this } } }); - MODx.panel.Dashboard.superclass.constructor.call(this,config); + MODx.panel.Dashboard.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Dashboard,MODx.FormPanel,{ - initialized: false +Ext.extend(MODx.panel.Dashboard, MODx.FormPanel, { + initialized: false, - ,setup: function() { + setup: function() { if (this.initialized) { return false; } if (Ext.isEmpty(this.config.record.id)) { this.fireEvent('ready'); @@ -154,45 +156,48 @@ Ext.extend(MODx.panel.Dashboard,MODx.FormPanel,{ this.getForm().setValues(this.config.record); Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(this.config.record.name)); - var d = this.config.record.widgets; - var g = Ext.getCmp('modx-grid-dashboard-widget-placements'); - if (d && g) { - g.getStore().loadData(d); + const + { widgets } = this.config.record, + placementsGrid = Ext.getCmp('modx-grid-dashboard-widget-placements') + ; + if (widgets && placementsGrid) { + placementsGrid.getStore().loadData(widgets); } - this.fireEvent('ready',this.config.record); + this.fireEvent('ready', this.config.record); MODx.fireEvent('ready'); this.initialized = true; - } + }, - ,beforeSubmit: function(o) { - var bp = {}; - var wg = Ext.getCmp('modx-grid-dashboard-widget-placements'); - if (wg) { - bp['widgets'] = wg.encode(); + beforeSubmit: function(o) { + const + params = {}, + placementsGrid = Ext.getCmp('modx-grid-dashboard-widget-placements') + ; + if (placementsGrid) { + params.widgets = placementsGrid.encode(); } - Ext.apply(o.form.baseParams,bp); - } + Ext.apply(o.form.baseParams, params); + }, - ,success: function(o) { + success: function(o) { if (Ext.isEmpty(this.config.record) || Ext.isEmpty(this.config.record.id)) { - MODx.loadPage('system/dashboards/update', 'id='+o.result.object.id); + MODx.loadPage('system/dashboards/update', `id=${o.result.object.id}`); } else { Ext.getCmp('modx-abtn-save').setDisabled(false); - var wg = Ext.getCmp('modx-grid-dashboard-widget-placements'); + const wg = Ext.getCmp('modx-grid-dashboard-widget-placements'); if (wg) { wg.getStore().commitChanges(); } - } - } + }, - ,getPageHeader: function(config) { + getPageHeader: function(config) { return MODx.util.getHeaderBreadCrumbs('modx-dashboard-header', [{ text: _('dashboards'), href: MODx.getPage('system/dashboards') }]); } }); -Ext.reg('modx-panel-dashboard',MODx.panel.Dashboard); +Ext.reg('modx-panel-dashboard', MODx.panel.Dashboard); /** * @class MODx.grid.DashboardWidgetPlacements @@ -200,109 +205,138 @@ Ext.reg('modx-panel-dashboard',MODx.panel.Dashboard); * @param {Object} config An object of configuration properties * @xtype modx-grid-dashboard-widget-placements */ -MODx.grid.DashboardWidgetPlacements = function(config) { - config = config || {}; +MODx.grid.DashboardWidgetPlacements = function(config = {}) { this.exp = new Ext.grid.RowExpander({ - tpl : new Ext.Template( + tpl: new Ext.Template( '

          {description_trans}

          ' ) }); - Ext.applyIf(config,{ - id: 'modx-grid-dashboard-widget-placements' - ,url: MODx.config.connector_url - ,action: 'system/dashboard/widget/placement/getList' - ,fields: ['dashboard','widget','rank','name','name_trans','description','description_trans'] - ,autoHeight: true - ,primaryKey: 'widget' - ,cls: 'modx-grid modx-grid-draggable' - ,plugins: [this.exp,new Ext.ux.dd.GridDragDropRowOrder({ - copy: false // false by default - ,scrollable: true // enable scrolling support (default is false) - ,targetCfg: {} - ,listeners: { - 'afterrowmove': {fn:this.onAfterRowMove,scope:this} + Ext.applyIf(config, { + id: 'modx-grid-dashboard-widget-placements', + url: MODx.config.connector_url, + action: 'system/dashboard/widget/placement/getList', + fields: [ + 'dashboard', + 'widget', + 'rank', + 'name', + 'name_trans', + 'description', + 'description_trans' + ], + autoHeight: true, + primaryKey: 'widget', + cls: 'modx-grid modx-grid-draggable', + plugins: [this.exp, new Ext.ux.dd.GridDragDropRowOrder({ + scrollable: true, + targetCfg: {}, + listeners: { + afterrowmove: { + fn: this.onAfterRowMove, + scope: this + } } - })] - ,columns: [this.exp,{ - header: _('widget') - ,dataIndex: 'name_trans' - ,width: 600 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=system/dashboards/widget/update&id=' + record.data.widget - ,target: '_blank' - }); - }, scope: this } - },{ - header: _('rank') - ,dataIndex: 'rank' - ,width: 80 - ,editor: { xtype: 'numberfield', allowBlank: false, allowNegative: false } - }] - ,tbar: [{ - text: _('widget_place') - ,cls:'primary-button' - ,handler: this.placeWidget - ,scope: this + })], + columns: [this.exp, { + header: _('widget'), + dataIndex: 'name_trans', + width: 150, + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=system/dashboards/widget/update&id=${record.data.widget}`, + target: '_blank' + }); + }, + scope: this + } + }, { + header: _('rank'), + dataIndex: 'rank', + width: 80, + align: 'center', + editor: { + xtype: 'numberfield', + allowBlank: false, + allowNegative: false + } + }], + tbar: [{ + text: _('widget_place'), + cls: 'primary-button', + handler: this.placeWidget, + scope: this }] }); - MODx.grid.DashboardWidgetPlacements.superclass.constructor.call(this,config); - this.propRecord = Ext.data.Record.create(['dashboard','widget','rank','name','name_trans','description','description_trans']); + MODx.grid.DashboardWidgetPlacements.superclass.constructor.call(this, config); + this.propRecord = Ext.data.Record.create([ + 'dashboard', + 'widget', + 'rank', + 'name', + 'name_trans', + 'description', + 'description_trans' + ]); }; -Ext.extend(MODx.grid.DashboardWidgetPlacements,MODx.grid.LocalGrid,{ +Ext.extend(MODx.grid.DashboardWidgetPlacements, MODx.grid.LocalGrid, { getMenu: function() { return [{ - text: _('widget_unplace') - ,handler: this.unplaceWidget - ,scope: this + text: _('widget_unplace'), + handler: this.unplaceWidget, + scope: this }]; - } + }, - ,onAfterRowMove: function(dt,sri,ri,sels) { - var s = this.getStore(); - var sourceRec = s.data.items[sri]; - var total = s.data.length; + onAfterRowMove: function(dropTarget, fromRowIndex, toRowIndex, selections) { + const + store = this.getStore(), + firstDraggedRecord = store.data.items[fromRowIndex], + total = store.data.length + ; + firstDraggedRecord.set('rank', fromRowIndex); + firstDraggedRecord.commit(); - sourceRec.set('rank',sri); - sourceRec.commit(); - - /* get all rows below ri, and up their rank by 1 */ - var brec; - for (var x=(ri-1);x 0 - // Get the rank of the last record - ? s.data.items[s.data.length - 1].get('rank') + 1 - // Or set it to '0' if no record found - : 0; - - var fldStore = fld.getStore(); - var fldRi = fldStore.find('id',fld.getValue()); - var rec = fldStore.getAt(fldRi); - - if (id != '' && this.fp.getForm().isValid()) { - - if (this.fireEvent('success',{ - widget: fld.getValue() - ,dashboard: g.config.dashboard - ,name: rec.data.name - ,name_trans: rec.data.name_trans - ,description: rec.data.description - ,description_trans: rec.data.description_trans - ,rank: rank + const + // Get the rank of the last record or set it to '0' if no record found + rank = store.data.length > 0 + ? store.data.items[store.data.length - 1].get('rank') + 1 + : 0, + widgetStore = widgetField.getStore(), + widgetRowIndex = widgetStore.find('id', widgetField.getValue()), + record = widgetStore.getAt(widgetRowIndex) + ; + if (this.fp.getForm().isValid()) { + if (this.fireEvent('success', { + widget: widgetField.getValue(), + dashboard: widgetsGrid.config.dashboard, + name: record.data.name, + name_trans: record.data.name_trans, + description: record.data.description, + description_trans: record.data.description_trans, + rank: rank })) { this.fp.getForm().reset(); this.hide(); return true; } } else { - MODx.msg.alert(_('error'),_('widget_err_ns')); + MODx.msg.alert(_('error'), _('widget_err_ns')); } return true; } }); -Ext.reg('modx-window-dashboard-widget-place',MODx.window.DashboardWidgetPlace); - -/* -MODx.grid.DashboardUserGroups = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-grid-dashboard-usergroups' - ,url: MODx.config.connector_url - ,action: 'system/dashboard/group/getList' - ,fields: ['id','name'] - ,autoHeight: true - ,primaryKey: 'user' - ,columns: [{ - header: _('user_group') - ,dataIndex: 'name' - ,width: 600 - }] - ,tbar: [{ - text: _('dashboard_usergroup_add') - ,handler: this.addUserGroup - ,scope: this - }] - }); - MODx.grid.DashboardUserGroups.superclass.constructor.call(this,config); - this.propRecord = Ext.data.Record.create(['id','name']); -}; -Ext.extend(MODx.grid.DashboardUserGroups,MODx.grid.LocalGrid,{ - getMenu: function() { - return [{ - text: _('dashboard_usergroup_remove') - ,handler: this.remove.createDelegate(this,[{ - title: _('dashboard_usergroup_remove') - ,text: _('dashboard_usergroup_remove_confirm') - }]) - ,scope: this - }]; - } - - ,addUserGroup: function(btn,e) { - this.loadWindow(btn,e,{ - xtype: 'modx-window-dashboard-usergroup-add' - ,listeners: { - 'success': {fn:function(vs) { - var rec = new this.propRecord(vs); - this.getStore().add(rec); - },scope:this} - } - }); - var w = Ext.getCmp('modx-window-dashboard-usergroup-add'); - w.reset(); - w.setValues({ - dashboard: this.config.dashboard - }); - - } -}); -Ext.reg('modx-grid-dashboard-usergroups',MODx.grid.DashboardUserGroups); - -MODx.window.DashboardUserGroupAdd = function(config) { - config = config || {}; - this.ident = config.ident || 'dbugadd'+Ext.id(); - Ext.applyIf(config,{ - title: _('dashboard_usergroup_add') - ,frame: true - ,id: 'modx-window-dashboard-usergroup-add' - ,fields: [{ - xtype: 'modx-combo-usergroup' - ,fieldLabel: _('user_group') - ,name: 'usergroup' - ,hiddenName: 'usergroup' - ,id: 'modx-'+this.ident+'-usergroup' - ,allowBlank: false - }] - }); - MODx.window.DashboardUserGroupAdd.superclass.constructor.call(this,config); -}; -Ext.extend(MODx.window.DashboardUserGroupAdd,MODx.Window,{ - submit: function() { - var f = this.fp.getForm(); - var fld = f.findField('usergroup'); - - if (id != '' && this.fp.getForm().isValid()) { - if (this.fireEvent('success',{ - id: fld.getValue() - ,name: fld.getRawValue() - })) { - this.fp.getForm().reset(); - this.hide(); - return true; - } - } else { - MODx.msg.alert(_('error'),_('user_group_err_ns')); - } - return true; - } -}); -Ext.reg('modx-window-dashboard-usergroup-add',MODx.window.DashboardUserGroupAdd); -*/ +Ext.reg('modx-window-dashboard-widget-place', MODx.window.DashboardWidgetPlace); /** * @class MODx.combo.DashboardWidgets @@ -486,28 +422,30 @@ Ext.reg('modx-window-dashboard-usergroup-add',MODx.window.DashboardUserGroupAdd) * @param {Object} config An object of options. * @xtype modx-combo-dashboard-widgets */ -MODx.combo.DashboardWidgets = function(config) { - config = config || {}; - Ext.applyIf(config,{ - name: 'widget' - ,hiddenName: 'widget' - ,displayField: 'name_trans' - ,editable: true - ,valueField: 'id' - ,fields: ['id','name','name_trans','description','description_trans'] - ,pageSize: 20 - ,url: MODx.config.connector_url - ,baseParams: { - action: 'System/Dashboard/Widget/GetList' - ,combo: true - } - ,tpl: new Ext.XTemplate('' - ,'
          ' - ,'

          {name_trans:htmlEncode}

          ' - ,'

          {description_trans:htmlEncode}

          ' - ,'
          ') +MODx.combo.DashboardWidgets = function(config = {}) { + Ext.applyIf(config, { + name: 'widget', + hiddenName: 'widget', + displayField: 'name_trans', + editable: true, + valueField: 'id', + fields: ['id', 'name', 'name_trans', 'description', 'description_trans'], + pageSize: 20, + url: MODx.config.connector_url, + baseParams: { + action: 'System/Dashboard/Widget/GetList', + combo: true + }, + tpl: new Ext.XTemplate(` + +
          +

          {name_trans:htmlEncode}

          +

          {description_trans:htmlEncode}

          +
          +
          + `) }); - MODx.combo.DashboardWidgets.superclass.constructor.call(this,config); + MODx.combo.DashboardWidgets.superclass.constructor.call(this, config); }; -Ext.extend(MODx.combo.DashboardWidgets,MODx.combo.ComboBox); -Ext.reg('modx-combo-dashboard-widgets',MODx.combo.DashboardWidgets); +Ext.extend(MODx.combo.DashboardWidgets, MODx.combo.ComboBox); +Ext.reg('modx-combo-dashboard-widgets', MODx.combo.DashboardWidgets); diff --git a/manager/controllers/default/system/dashboards/update.class.php b/manager/controllers/default/system/dashboards/update.class.php index b7b5378149..3e52b2ef21 100644 --- a/manager/controllers/default/system/dashboards/update.class.php +++ b/manager/controllers/default/system/dashboards/update.class.php @@ -1,4 +1,5 @@ modx->hasPermission('dashboards'); } @@ -40,8 +43,9 @@ public function checkPermissions() { * * @return array */ - public function process(array $scriptProperties = []) { - if (empty($this->scriptProperties['id']) || strlen($this->scriptProperties['id']) !== strlen((integer)$this->scriptProperties['id'])) { + public function process(array $scriptProperties = []) + { + if (empty($this->scriptProperties['id']) || strlen($this->scriptProperties['id']) !== strlen((int)$this->scriptProperties['id'])) { $this->failure($this->modx->lexicon('dashboard_err_ns')); return []; } @@ -55,30 +59,28 @@ public function process(array $scriptProperties = []) { $this->dashboardArray['widgets'] = $this->getWidgets(); return $this->dashboardArray; - } /** * Get all the Widgets placed on this Dashboard * @return array */ - public function getWidgets() { + public function getWidgets() + { $c = $this->modx->newQuery(modDashboardWidgetPlacement::class); $c->where([ 'dashboard' => $this->dashboard->get('id'), 'user' => 0, ]); - $c->sortby('modDashboardWidgetPlacement.rank','ASC'); + $c->sortby('modDashboardWidgetPlacement.rank', 'ASC'); $placements = $this->modx->getCollection(modDashboardWidgetPlacement::class, $c); $list = []; /** @var modDashboardWidgetPlacement $placement */ foreach ($placements as $placement) { $placement->getOne('Widget'); - if (!($placement->Widget instanceof modDashboardWidget)) { continue; } - if ($placement->Widget->get('lexicon') != 'core:dashboards') { $this->modx->lexicon->load($placement->Widget->get('lexicon')); } @@ -100,7 +102,8 @@ public function getWidgets() { * Get all the User Groups assigned to this Dashboard * @return array */ - public function getUserGroups() { + public function getUserGroups() + { $list = []; $c = $this->modx->newQuery(modUserGroup::class); $c->where([ @@ -121,9 +124,10 @@ public function getUserGroups() { * Register custom CSS/JS for the page * @return void */ - public function loadCustomCssJs() { - $this->addJavascript($this->modx->getOption('manager_url')."assets/modext/widgets/system/modx.panel.dashboard.js"); - $this->addJavascript($this->modx->getOption('manager_url').'assets/modext/sections/system/dashboards/update.js'); + public function loadCustomCssJs() + { + $this->addJavascript($this->modx->getOption('manager_url') . "assets/modext/widgets/system/modx.panel.dashboard.js"); + $this->addJavascript($this->modx->getOption('manager_url') . 'assets/modext/sections/system/dashboards/update.js'); $data = json_encode([ 'xtype' => 'modx-page-dashboard-update', 'record' => $this->dashboardArray, @@ -136,15 +140,17 @@ public function loadCustomCssJs() { * * @return string */ - public function getPageTitle() { - return $this->modx->lexicon('dashboards').': '.$this->dashboardArray['name']; + public function getPageTitle() + { + return $this->modx->lexicon('dashboards') . ': ' . $this->dashboardArray['name']; } /** * Return the location of the template file * @return string */ - public function getTemplateFile() { + public function getTemplateFile() + { return ''; } @@ -152,7 +158,8 @@ public function getTemplateFile() { * Specify the language topics to load * @return array */ - public function getLanguageTopics() { + public function getLanguageTopics() + { return ['dashboards','user']; } @@ -160,7 +167,8 @@ public function getLanguageTopics() { * Get the Help URL * @return string */ - public function getHelpUrl() { + public function getHelpUrl() + { return 'Dashboards'; } } From a94232bb6df3e32e33db37de50e811b5cceb15ca Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 2 Nov 2024 12:08:55 -0400 Subject: [PATCH 08/39] Dashboard(s) updates Implements new permissions handling and fixes a couple other issues in the Dashboard editing panel: - Added validation to prevent dup Dashboard names - Styled toggles to match rest of current UI --- core/lexicon/en/dashboards.inc.php | 22 +- .../Processors/System/Dashboard/GetList.php | 50 ++- .../Processors/System/Dashboard/Update.php | 30 ++ core/src/Revolution/modDashboard.php | 27 +- .../widgets/system/modx.panel.dashboard.js | 85 ++-- .../widgets/system/modx.panel.dashboards.js | 362 +++++++++--------- .../system/dashboards/update.class.php | 8 + 7 files changed, 362 insertions(+), 222 deletions(-) diff --git a/core/lexicon/en/dashboards.inc.php b/core/lexicon/en/dashboards.inc.php index f6cb29fd2d..c7eab7bf0b 100644 --- a/core/lexicon/en/dashboards.inc.php +++ b/core/lexicon/en/dashboards.inc.php @@ -7,16 +7,19 @@ * @language en */ $_lang['dashboard'] = 'Dashboard'; -$_lang['dashboard_desc_name'] = 'The name of the Dashboard.'; -$_lang['dashboard_desc_description'] = 'A short description of the Dashboard.'; -$_lang['dashboard_desc_hide_trees'] = 'Checking this will hide the left-hand trees when this Dashboard is rendered on the welcome page.'; -$_lang['dashboard_hide_trees'] = 'Hide Left-Hand Trees'; -$_lang['dashboard_desc_customizable'] = 'Allow users to customize this dashboard for their accounts: create, delete and change position or size of widgets.'; $_lang['dashboard_customizable'] = 'Customizable'; +$_lang['dashboard_customizable_desc'] = 'Allow users to customize this dashboard for their accounts: create, delete and change position or size of widgets.'; +$_lang['dashboard_description_desc'] = 'A short description of the Dashboard.'; +$_lang['dashboard_edit'] = 'Edit the settings and Widget placements for this Dashboard'; +$_lang['dashboard_hide_trees'] = 'Hide Left-Hand Trees'; +$_lang['dashboard_hide_trees_desc'] = 'Checking this will hide the left-hand trees when this Dashboard is rendered on the welcome page.'; +$_lang['dashboard_name_desc'] = 'The name of the Dashboard.'; $_lang['dashboard_remove_confirm'] = 'Are you sure you want to delete this Dashboard?'; $_lang['dashboard_remove_multiple_confirm'] = 'Are you sure you want to delete the selected Dashboards?'; +$_lang['dashboard_reserved_general_desc'] = 'Note that this is a protected, built-in Dashboard. Its general values are locked, but other specifications (such as assigned Widgets) are editable by users with the appropriate permissions.'; $_lang['dashboard_err_ae_name'] = 'A dashboard with the name "[[+name]]" already exists! Please try another name.'; $_lang['dashboard_err_duplicate'] = 'An error occurred while trying to duplicate the dashboard.'; +$_lang['dashboard_err_name_reserved'] = 'The dashboard name “[[+reservedName]]” is reserved. Please choose another name.'; $_lang['dashboard_err_nf'] = 'Dashboard not found.'; $_lang['dashboard_err_ns'] = 'Dashboard not specified.'; $_lang['dashboard_err_ns_name'] = 'Please specify a name for the widget.'; @@ -27,8 +30,9 @@ $_lang['dashboard_usergroup_remove'] = 'Delete Dashboard from User Group'; $_lang['dashboard_usergroup_remove_confirm'] = 'Are you sure you want to revert this User Group to using the default Dashboard?'; $_lang['dashboard_usergroups.intro_msg'] = 'Here is a list of all the User Groups using this Dashboard.'; +$_lang['dashboard_widget_edit'] = 'Edit this Widget’s specifications and properties'; $_lang['dashboard_widget_err_placed'] = 'This widget is already placed in this Dashboard!'; -$_lang['dashboard_widgets.intro_msg'] = 'Manage widgets in this dashboard. You can also drag and drop rows in the grid to rearrange them.

          Please note: if a dashboard is "customizable", this settings will be applied only for the first load for every user. From here they will be able to create, delete and change the position or size of their widgets. User access to widgets can be limited by applying permissions.'; +$_lang['dashboard_widgets.intro_msg'] = 'Manage the widgets to be showin in this dashboard. Widgets may be re-ordered by changing their Rank or by dragging and dropping their grid rows into the desired position.

          Note that if a dashboard is “Customizable,” its initial settings will only apply until a user adds, removes, or makes other changes to the dashboard’s widgets. User access to widgets can be limited by applying permissions.'; $_lang['dashboards'] = 'Dashboards'; $_lang['dashboards.intro_msg'] = 'Here you can manage all the available Dashboards for this MODX manager.'; $_lang['rank'] = 'Rank'; @@ -100,3 +104,9 @@ $_lang['w_whosonline_desc'] = 'Shows a list of online users.'; $_lang['w_view_all'] = 'View all'; $_lang['w_no_data'] = 'No data to display'; + +// Temporarily match old keys to new ones to ensure compatibility +$_lang['dashboard_desc_customizable'] = $_lang['dashboard_customizable_desc']; +$_lang['dashboard_desc_name'] = $_lang['dashboard_name_desc']; +$_lang['dashboard_desc_description'] = $_lang['dashboard_description_desc']; +$_lang['dashboard_desc_hide_trees'] = $_lang['dashboard_hide_trees_desc']; diff --git a/core/src/Revolution/Processors/System/Dashboard/GetList.php b/core/src/Revolution/Processors/System/Dashboard/GetList.php index 199b1b0091..7f24bb9164 100644 --- a/core/src/Revolution/Processors/System/Dashboard/GetList.php +++ b/core/src/Revolution/Processors/System/Dashboard/GetList.php @@ -1,4 +1,5 @@ setDefaultProperties([ + 'query' => '', + 'exclude' => 'creator' + ]); + $canManage = $this->modx->hasPermission('dashboards'); + $this->canCreate = $canManage; + $this->canEdit = $canManage; + $this->canRemove = $canManage; + $this->coreDashboards = $this->classKey::getCoreDashboards(); + + return $initialized; + } + /** * @param xPDOQuery $c * @return xPDOQuery @@ -61,13 +87,29 @@ public function prepareQueryAfterCount(xPDOQuery $c) } /** - * @param xPDOObject $object + * @param xPDOObject|modDashboard $object * @return array */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); - $objectArray['cls'] = 'pupdate premove pduplicate'; - return $objectArray; + $permissions = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + $dashboardData = $object->toArray(); + $dashboardName = $object->get('name'); + $isCoreDashboard = $object->isCoreDashboard($dashboardName); + + $dashboardData['reserved'] = ['name' => $this->coreDashboards]; + $dashboardData['isProtected'] = $isCoreDashboard; + $dashboardData['creator'] = $isCoreDashboard ? 'modx' : strtolower($this->modx->lexicon('user')) ; + if ($isCoreDashboard) { + unset($permissions['delete']); + } + $dashboardData['permissions'] = $permissions; + + return $dashboardData; } } diff --git a/core/src/Revolution/Processors/System/Dashboard/Update.php b/core/src/Revolution/Processors/System/Dashboard/Update.php index aed74a45ea..7ce6ee217e 100644 --- a/core/src/Revolution/Processors/System/Dashboard/Update.php +++ b/core/src/Revolution/Processors/System/Dashboard/Update.php @@ -30,6 +30,36 @@ class Update extends UpdateProcessor /** @var modDashboard $object */ public $object; + public function beforeSave() + { + /* validate name field */ + $name = $this->object->get('name'); + $id = $this->object->get('id'); + + if (empty($name)) { + $this->addFieldError('name', $this->modx->lexicon('dashboard_err_ns_name')); + } elseif ($this->alreadyExists($name, $id)) { + $this->addFieldError('name', $this->modx->lexicon('dashboard_err_ae_name', [ + 'name' => $name, + ])); + } + + return parent::beforeSave(); + } + + /** + * Check to see if a Dashboard with the specified name already exists + * @param string $name + * @return boolean + */ + public function alreadyExists($name, $id) + { + return $this->modx->getCount(modDashboard::class, [ + 'name' => $name, + 'id:!=' => $id + ]) > 0; + } + /** * @return bool */ diff --git a/core/src/Revolution/modDashboard.php b/core/src/Revolution/modDashboard.php index 4949054f1f..d0fbd7243e 100644 --- a/core/src/Revolution/modDashboard.php +++ b/core/src/Revolution/modDashboard.php @@ -19,6 +19,8 @@ */ class modDashboard extends xPDOSimpleObject { + public const DASHBOARD_DEFAULT = 'Default'; + /** * Get the default MODX dashboard * @@ -73,7 +75,6 @@ public function remove(array $ancestors = []) return $removed; } - /** * Render the Dashboard * @@ -129,7 +130,6 @@ public function render(modManagerController $controller, $user = null) return implode("\n", $output); } - /** * @param int $user * @param bool $force @@ -168,7 +168,6 @@ public function sortWidgets($user = 0, $force = false) } } - /** * @param modUser $user */ @@ -188,4 +187,26 @@ protected function addUserWidgets(modUser $user) $new->save(); } } + + /** + * Returns a list of core Dashboards + * + * @return array + */ + public static function getCoreDashboards(): array + { + return [ + self::DASHBOARD_DEFAULT + ]; + } + + /** + * @param string $name The name of the Dashboard + * + * @return bool + */ + public function isCoreDashboard($name): bool + { + return in_array($name, static::getCoreDashboards(), true); + } } diff --git a/manager/assets/modext/widgets/system/modx.panel.dashboard.js b/manager/assets/modext/widgets/system/modx.panel.dashboard.js index 9fdb351107..081571983c 100644 --- a/manager/assets/modext/widgets/system/modx.panel.dashboard.js +++ b/manager/assets/modext/widgets/system/modx.panel.dashboard.js @@ -5,6 +5,14 @@ * @xtype modx-panel-dashboard */ MODx.panel.Dashboard = function(config = {}) { + let generalIntro = {}; + if (config.record.reserved) { + generalIntro = { + xtype: 'box', + cls: 'panel-desc', + html: _('dashboard_reserved_general_desc') + }; + } Ext.applyIf(config, { id: 'modx-panel-dashboard', url: MODx.config.connector_url, @@ -12,7 +20,10 @@ MODx.panel.Dashboard = function(config = {}) { action: 'System/Dashboard/Update' }, cls: 'container', - defaults: { collapsible: false, autoHeight: true }, + defaults: { + collapsible: false, + autoHeight: true + }, items: [this.getPageHeader(config), { xtype: 'modx-tabs', defaults: { @@ -32,11 +43,14 @@ MODx.panel.Dashboard = function(config = {}) { items: [{ title: _('general_information'), cls: 'form-with-labels', - defaults: { border: false, cls: 'main-wrapper' }, + defaults: { + border: false, + cls: 'main-wrapper' + }, layout: 'form', id: 'modx-dashboard-form', labelAlign: 'top', - items: [{ + items: [generalIntro, { xtype: 'hidden', name: 'id', id: 'modx-dashboard-id', @@ -47,21 +61,23 @@ MODx.panel.Dashboard = function(config = {}) { defaults: { layout: 'form', labelAlign: 'top', - anchor: '100%', + labelSeparator: '', border: false }, items: [{ columnWidth: 0.7, cls: 'main-content', + defaults: { + msgTarget: 'under', + anchor: '100%' + }, items: [{ + xtype: config.record.reserved ? 'statictextfield' : 'textfield', name: 'name', - id: 'modx-dashboard-name', - xtype: 'textfield', fieldLabel: _('name'), - description: MODx.expandHelp ? '' : _('dashboard_desc_name'), + description: MODx.expandHelp ? '' : _('dashboard_name_desc'), allowBlank: false, enableKeyEvents: true, - anchor: '100%', listeners: { keyup: { fn: function(f, e) { @@ -71,22 +87,24 @@ MODx.panel.Dashboard = function(config = {}) { } } }, { - xtype: MODx.expandHelp ? 'label' : 'hidden', - forId: 'modx-dashboard-name', - html: _('dashboard_desc_name'), + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('dashboard_name_desc'), cls: 'desc-under' }, { name: 'description', - id: 'modx-dashboard-description', xtype: 'textarea', + /** + * @todo - Change this xtype to the following once Lexicon-based name/desc is implemented for core dashboard + * xtype: config.record.reserved ? 'statictextfield' : 'textfield', + */ fieldLabel: _('description'), - description: MODx.expandHelp ? '' : _('dashboard_desc_description'), - anchor: '100%', + description: MODx.expandHelp ? '' : _('dashboard_description_desc'), grow: true }, { - xtype: MODx.expandHelp ? 'label' : 'hidden', - forId: 'modx-dashboard-description', - html: _('dashboard_desc_description'), + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('dashboard_description_desc'), cls: 'desc-under' }] }, { @@ -94,28 +112,28 @@ MODx.panel.Dashboard = function(config = {}) { cls: 'main-content', items: [{ name: 'hide_trees', - id: 'modx-dashboard-hide-trees', xtype: 'xcheckbox', + ctCls: 'display-switch', boxLabel: _('dashboard_hide_trees'), - description: MODx.expandHelp ? '' : _('dashboard_desc_hide_trees'), + description: MODx.expandHelp ? '' : _('dashboard_hide_trees_desc'), inputValue: 1 }, { - xtype: MODx.expandHelp ? 'label' : 'hidden', - forId: 'modx-dashboard-hide-trees', - html: _('dashboard_desc_hide_trees'), + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('dashboard_hide_trees_desc'), cls: 'desc-under' }, { name: 'customizable', - id: 'modx-dashboard-customizable', xtype: 'xcheckbox', + ctCls: 'display-switch', boxLabel: _('dashboard_customizable'), - description: MODx.expandHelp ? '' : _('dashboard_desc_customizable'), + description: MODx.expandHelp ? '' : _('dashboard_customizable_desc'), inputValue: 1, checked: true }, { - xtype: MODx.expandHelp ? 'label' : 'hidden', - forId: 'modx-dashboard-customizable', - html: _('dashboard_desc_customizable'), + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('dashboard_customizable_desc'), cls: 'desc-under' }] }] @@ -378,10 +396,11 @@ Ext.extend(MODx.window.DashboardWidgetPlace, MODx.Window, { const form = this.fp.getForm(), widgetField = form.findField('widget'), + selectedWidget = widgetField.getValue(), widgetsGrid = Ext.getCmp('modx-grid-dashboard-widget-placements'), store = widgetsGrid.getStore() ; - if (store.find('widget', widgetField.getValue()) !== -1) { + if (store.find('widget', selectedWidget) !== -1) { widgetField.markInvalid(_('dashboard_widget_err_placed')); return false; } @@ -391,7 +410,7 @@ Ext.extend(MODx.window.DashboardWidgetPlace, MODx.Window, { ? store.data.items[store.data.length - 1].get('rank') + 1 : 0, widgetStore = widgetField.getStore(), - widgetRowIndex = widgetStore.find('id', widgetField.getValue()), + widgetRowIndex = widgetStore.find('id', selectedWidget), record = widgetStore.getAt(widgetRowIndex) ; if (this.fp.getForm().isValid()) { @@ -429,7 +448,13 @@ MODx.combo.DashboardWidgets = function(config = {}) { displayField: 'name_trans', editable: true, valueField: 'id', - fields: ['id', 'name', 'name_trans', 'description', 'description_trans'], + fields: [ + 'id', + 'name', + 'name_trans', + 'description', + 'description_trans' + ], pageSize: 20, url: MODx.config.connector_url, baseParams: { diff --git a/manager/assets/modext/widgets/system/modx.panel.dashboards.js b/manager/assets/modext/widgets/system/modx.panel.dashboards.js index 1be48b36c5..216697ba1c 100644 --- a/manager/assets/modext/widgets/system/modx.panel.dashboards.js +++ b/manager/assets/modext/widgets/system/modx.panel.dashboards.js @@ -4,44 +4,46 @@ * @param {Object} config An object of configuration properties * @xtype modx-panel-dashboards */ -MODx.panel.Dashboards = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-dashboards' - ,cls: 'container' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('dashboards') - ,id: 'modx-dashboards-header' - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - layout: 'form' - ,title: _('dashboards') - ,items: [{ - html: '

          '+_('dashboards.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-dashboards' - ,cls: 'main-wrapper' - ,preventRender: true +MODx.panel.Dashboards = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-dashboards', + cls: 'container', + defaults: { + collapsible: false, + autoHeight: true + }, + items: [{ + html: _('dashboards'), + id: 'modx-dashboards-header', + xtype: 'modx-header' + }, MODx.getPageStructure([{ + layout: 'form', + title: _('dashboards'), + items: [{ + html: `

          ${_('dashboards.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-dashboards', + cls: 'main-wrapper', + preventRender: true }] - },{ - layout: 'form' - ,title: _('widgets') - ,items: [{ - html: '

          '+_('widgets.intro_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-dashboard-widgets' - ,cls: 'main-wrapper' - ,preventRender: true + }, { + layout: 'form', + title: _('widgets'), + items: [{ + html: `

          ${_('widgets.intro_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-dashboard-widgets', + cls: 'main-wrapper', + preventRender: true }] }])] }); - MODx.panel.Dashboards.superclass.constructor.call(this,config); + MODx.panel.Dashboards.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Dashboards,MODx.FormPanel); -Ext.reg('modx-panel-dashboards',MODx.panel.Dashboards); +Ext.extend(MODx.panel.Dashboards, MODx.FormPanel); +Ext.reg('modx-panel-dashboards', MODx.panel.Dashboards); /** * @class MODx.grid.Dashboards @@ -52,72 +54,108 @@ Ext.reg('modx-panel-dashboards',MODx.panel.Dashboards); MODx.grid.Dashboards = function(config = {}) { const queryValue = this.applyRequestFilter(0, 'query', 'tab', true); this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + id: 'modx-grid-dashboards', + url: MODx.config.connector_url, + baseParams: { action: 'System/Dashboard/GetList', usergroup: MODx.request.usergroup || null - } - ,fields: [ + }, + fields: [ 'id', 'name', 'description', - 'cls' - ] - ,paging: true - ,autosave: true - ,save_action: 'System/Dashboard/UpdateFromGrid' - ,remoteSort: true - ,sm: this.sm - ,columns: [this.sm,{ - header: _('id') - ,dataIndex: 'id' - ,width: 50 - ,sortable: true - },{ - header: _('name') - ,dataIndex: 'name' - ,width: 150 - ,sortable: true - ,editor: { xtype: 'textfield' ,allowBlank: false } - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=system/dashboards/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 300 - ,sortable: false - ,editor: { xtype: 'textarea' } - }] - ,tbar: [ - { - text: _('create') - ,cls:'primary-button' - ,handler: this.createDashboard - ,scope: this - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this - }] - },'->',{ - xtype: 'modx-combo-usergroup' - ,itemId: 'filter-usergroup' - ,emptyText: _('user_group_filter') - ,baseParams: { - action: 'Security/Group/GetList' - ,addAll: true + 'creator' + ], + paging: true, + autosave: true, + save_action: 'System/Dashboard/UpdateFromGrid', + remoteSort: true, + sm: this.sm, + columns: [this.sm, { + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('name'), + dataIndex: 'name', + id: 'modx-dashboard--name', + width: 150, + sortable: true, + editor: { + xtype: 'textfield', + allowBlank: false, + blankText: _('dashboard_err_ns_name'), + validationEvent: 'change', + validator: function(value) { + const grid = Ext.getCmp('modx-grid-dashboards'), + reserved = this.gridEditor.record.json.reserved.name + ; + if (grid.valueIsReserved(reserved, value)) { + const msg = _('dashboard_err_name_reserved', { reservedName: value }); + Ext.Msg.alert(_('error'), msg); + return false; + } + return true; } - ,value: MODx.request.usergroup || null - ,width: 200 - ,listeners: { + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=system/dashboards/update&id=${record.data.id}`, + title: _('dashboard_edit') + }) + : value + ; + }, + scope: this + } + }, { + header: _('description'), + dataIndex: 'description', + id: 'modx-dashboard--description', + width: 300, + sortable: false, + editor: { + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return value; + }, + scope: this + } + }, + this.getCreatorColumnConfig('dashboard') + ], + tbar: [ + { + text: _('create'), + cls: 'primary-button', + handler: this.createDashboard, + scope: this + }, + this.getBulkActionsButton('dashboard', 'System/Dashboard/RemoveMultiple'), + '->', + { + xtype: 'modx-combo-usergroup', + itemId: 'filter-usergroup', + emptyText: _('user_group_filter'), + baseParams: { + action: 'Security/Group/GetList', + addAll: true + }, + value: MODx.request.usergroup || null, + width: 200, + listeners: { select: { - fn: function (cmp, record, selectedIndex) { + fn: function(cmp, record, selectedIndex) { this.applyGridFilter(cmp, 'usergroup'); }, scope: this @@ -126,105 +164,71 @@ MODx.grid.Dashboards = function(config = {}) { }, this.getQueryFilterField(`filter-query:${queryValue}`), this.getClearFiltersButton('filter-usergroup, filter-query') - ] + ], + viewConfig: this.getViewConfig() }); - MODx.grid.Dashboards.superclass.constructor.call(this,config); + MODx.grid.Dashboards.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate']; + + // Note there are currently no action-specific permissions for Dashboards + this.setUserCanEdit(['dashboards']); + this.setUserCanCreate(['dashboards']); + this.setUserCanDelete(['dashboards']); + this.setShowActionsMenu(); }; -Ext.extend(MODx.grid.Dashboards,MODx.grid.Grid,{ +Ext.extend(MODx.grid.Dashboards, MODx.grid.Grid, { getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; - if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.updateDashboard }); - } else { - if (p.indexOf('pupdate') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateDashboard - }); - } - if (p.indexOf('pduplicate') != -1) { - m.push({ - text: _('duplicate') - ,handler: this.duplicateDashboard - }); - } - if (p.indexOf('premove') != -1 && r.data.id != 1 && r.data.name != 'Default') { - if (m.length > 0) m.push('-'); - m.push({ - text: _('delete') - ,handler: this.removeDashboard - }); - } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (this.userCanCreate && this.userCanDuplicateRecord(record)) { + menu.push({ + text: _('duplicate'), + handler: this.duplicateDashboard + }); } - } + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('delete'), + handler: this.confirm.createDelegate(this, ['System/Dashboard/Remove', 'dashboard_remove_confirm']) + }); + } + return menu; + }, - ,createDashboard: function() { + createDashboard: function() { MODx.loadPage('system/dashboards/create'); - } + }, - ,updateDashboard: function() { - MODx.loadPage('system/dashboards/update', 'id='+this.menu.record.id); - } + updateDashboard: function() { + MODx.loadPage('system/dashboards/update', `id=${this.menu.record.id}`); + }, - ,duplicateDashboard: function(btn,e) { + duplicateDashboard: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'System/Dashboard/Duplicate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} - } - }); - } - - ,removeDashboard: function() { - MODx.msg.confirm({ - title: _('delete') - ,text: _('dashboard_remove_confirm') - ,url: this.config.url - ,params: { - action: 'System/Dashboard/Remove' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} - } - }); - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; - - MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('dashboard_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'System/Dashboard/RemoveMultiple' - ,dashboards: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'System/Dashboard/Duplicate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - return true; } - }); -Ext.reg('modx-grid-dashboards',MODx.grid.Dashboards); +Ext.reg('modx-grid-dashboards', MODx.grid.Dashboards); diff --git a/manager/controllers/default/system/dashboards/update.class.php b/manager/controllers/default/system/dashboards/update.class.php index 3e52b2ef21..790d2c985b 100644 --- a/manager/controllers/default/system/dashboards/update.class.php +++ b/manager/controllers/default/system/dashboards/update.class.php @@ -56,6 +56,14 @@ public function process(array $scriptProperties = []) } $this->dashboardArray = $this->dashboard->toArray(); + + $coreDashboards = modDashboard::getCoreDashboards(); + $dashboardKey = $this->dashboardArray['name']; + if (in_array($dashboardKey, $coreDashboards)) { + $this->dashboardArray['isProtected'] = true; + $this->dashboardArray['reserved'] = true; + } + $this->dashboardArray['widgets'] = $this->getWidgets(); return $this->dashboardArray; From 1e5a5f8a8abb1a40af0696e58be328da0e5bc1bd Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sun, 3 Nov 2024 00:03:29 -0400 Subject: [PATCH 09/39] ACL Policy & Policy Template updates --- core/lexicon/en/policy.inc.php | 6 + .../Security/Access/Policy/GetList.php | 63 +- .../Access/Policy/Template/GetList.php | 45 +- .../Revolution/modAccessPolicyTemplate.php | 27 +- .../security/modx.grid.access.policy.js | 608 +++++++++-------- .../modx.grid.access.policy.template.js | 622 +++++++++++------- 6 files changed, 801 insertions(+), 570 deletions(-) diff --git a/core/lexicon/en/policy.inc.php b/core/lexicon/en/policy.inc.php index 6d35864b35..3df209dbfe 100644 --- a/core/lexicon/en/policy.inc.php +++ b/core/lexicon/en/policy.inc.php @@ -28,6 +28,7 @@ $_lang['policy_desc_template'] = 'The Policy Template used for this Policy. Policies get their Permission lists from their Template.'; $_lang['policy_desc_lexicon'] = 'Optional. The Lexicon Topic that this Policy uses to translate the Permissions it owns.'; $_lang['policy_duplicate_confirm'] = 'Are you sure you want to duplicate this policy and all of its data?'; +$_lang['policy_edit'] = 'Edit the permissions assigned to this Policy'; $_lang['policy_err_ae'] = 'A Policy already exists with the name `[[+name]]`. Please select another name.'; $_lang['policy_err_nf'] = 'Policy not found.'; $_lang['policy_err_ns'] = 'Policy not specified.'; @@ -47,6 +48,7 @@ $_lang['policy_template_desc'] = 'A Policy Template defines which Permissions will show up in the Permissions grid when editing a specific Policy. You can add or remove specific Permissions from this template below. Note that removing a Permission from a Template will remove it from any Policies that use this Template.'; $_lang['policy_template_desc_name'] = 'The name of the Access Policy Template'; $_lang['policy_template_desc_description'] = 'Optional. A short description of the Access Policy Template. Also you might use lexicon keys here.'; +$_lang['policy_template_edit'] = 'Edit the permissions assigned to this Policy Template'; $_lang['policy_template_lexicon'] = 'Lexicon Topic'; $_lang['policy_template_desc_lexicon'] = 'Optional. The Lexicon Topic that this Policy Template uses to translate the Permissions it owns.'; $_lang['policy_template_desc_template_group'] = 'The Policy Template Group to use. This is used when selecting Policies from a dropdown menu; usually they are filtered by template group. Select an appropriate group for your Policy Template.'; @@ -61,6 +63,10 @@ $_lang['policy_template_remove_confirm_in_use'] = 'Are you sure you want to delete this Policy Template? It will delete all Policies attached to this Template as well - this could break your MODX installation if any active Policies are attached to this Template.

          This template is used by existing Policies ([[+count]] in total). Are you sure you want to delete this template and all attached policies?'; $_lang['policy_template_remove_multiple_confirm'] = 'Are you sure you want to delete these Policy Templates? It will delete all Policies attached to these Templates as well - this could break your MODX installation if any active Policies are attached to these Templates.'; $_lang['policy_template_remove_multiple_confirm_in_use'] = 'Are you sure you want to delete these Policy Templates? It will delete all Policies attached to these Templates as well - this could break your MODX installation if any active Policies are attached to these Templates.

          Some of selected templates are still used by existing Policies ([[+count]] in total). Are you sure you want to delete these template and all attached policies?'; +$_lang['policy_template_remove_multiple_confirm_in_use_ignoring_protected'] = 'In addition to the [[+count-templates]] Policy Templates you have selected, [[+count-policies]] Access Policies (attached to one or more of these Policy Templates) will be deleted. If any of these Access Policies are currently assigned to a permissions rule, you could break your MODX installation by removing them. (Note that the [[+protected]] protected Templates in your selection will not be removed.) +

          +Are you sure you want to continue? +'; $_lang['policy_templates'] = 'Policy Templates'; $_lang['policy_templates.intro_msg'] = 'This is a list of Policy Templates which define lists of Permissions that are checked or unchecked in specific Policies.'; $_lang['policy_template_administrator_desc'] = 'Context administration policy template with all permissions.'; diff --git a/core/src/Revolution/Processors/Security/Access/Policy/GetList.php b/core/src/Revolution/Processors/Security/Access/Policy/GetList.php index 94c420f6b5..0dcdfe5998 100644 --- a/core/src/Revolution/Processors/Security/Access/Policy/GetList.php +++ b/core/src/Revolution/Processors/Security/Access/Policy/GetList.php @@ -44,6 +44,13 @@ class GetList extends GetListProcessor /** @param boolean $isGridFilter Indicates the target of this list data is a filter field */ protected $isGridFilter = false; + public $canCreate = false; + public $canEdit = false; + public $canEditTemplate = false; + public $canRemove = false; + protected $corePolicies; + protected $corePolicyTemplates; + // private $templatesTranslated = []; /** * @return bool @@ -56,8 +63,17 @@ public function initialize() 'group' => false, 'combo' => false, 'query' => '', + 'exclude' => 'creator' ]); $this->isGridFilter = $this->getProperty('isGridFilter', false); + + $this->canCreate = $this->modx->hasPermission('policy_new') && $this->modx->hasPermission('policy_save'); + $this->canEdit = $this->modx->hasPermission('policy_edit'); + $this->canEditTemplate = $this->modx->hasPermission('policy_template_edit'); + $this->canRemove = $this->modx->hasPermission('policy_delete'); + $this->corePolicies = $this->classKey::getCorePolicies(); + $this->corePolicyTemplates = modAccessPolicyTemplate::getCoreTemplates(); + return $initialized; } @@ -206,45 +222,58 @@ public function beforeIteration(array $list) } /** - * @param xPDOObject $object + * @param xPDOObject|modAccessPolicy $object * @return array */ public function prepareRow(xPDOObject $object) { - $policy = $object->toArray(); + $permissions = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'updateTemplate' => $this->canEditTemplate, + 'delete' => $this->canRemove + ]; + $policyData = $object->toArray(); + $policyName = $object->get('name'); + $isCorePolicy = $object->isCorePolicy($policyName); + $this->setActivePermissionsCount($policyData, $object->get('data')); + + $policyData['reserved'] = ['name' => $this->corePolicies]; + $policyData['isProtected'] = $isCorePolicy; + $policyData['creator'] = $isCorePolicy ? 'modx' : strtolower($this->modx->lexicon('user')) ; + if ($isCorePolicy) { + unset($permissions['delete']); + } + $policyData['permissions'] = $permissions; + $policyData['description_trans'] = $this->modx->lexicon($policyData['description']); + unset($policyData['data']); - $policy['cls'] = $this->prepareRowClasses($object); + return $policyData; + } - $permissions = []; + protected function setActivePermissionsCount(array &$policy, array $data) + { if (!empty($policy['total_permissions'])) { - $data = $object->get('data'); - $ct = 0; + $n = 0; if (!empty($data)) { foreach ($data as $k => $v) { if (!empty($v)) { - $permissions[] = $k; - $ct++; + $n++; } } } - $policy['active_permissions'] = $ct; + $policy['active_permissions'] = $n; $policy['active_of'] = $this->modx->lexicon('active_of', [ 'active' => $policy['active_permissions'], 'total' => $policy['total_permissions'], ]); - $policy['permissions'] = $permissions; } - - unset($policy['data']); - - $policy['description_trans'] = $this->modx->lexicon($policy['description']); - - return $policy; } /** * @param xPDOObject|modAccessPolicy $object - * + * @deprecated as of 3.1 * @return string */ protected function prepareRowClasses(xPDOObject $object) diff --git a/core/src/Revolution/Processors/Security/Access/Policy/Template/GetList.php b/core/src/Revolution/Processors/Security/Access/Policy/Template/GetList.php index 877a7b5f32..8a27c21c54 100644 --- a/core/src/Revolution/Processors/Security/Access/Policy/Template/GetList.php +++ b/core/src/Revolution/Processors/Security/Access/Policy/Template/GetList.php @@ -36,6 +36,14 @@ class GetList extends GetListProcessor public $permission = 'policy_template_view'; public $languageTopics = ['policy', 'en:policy']; + /** @param boolean $isGridFilter Indicates the target of this list data is a filter field */ + protected $isGridFilter = false; + public $canCreate = false; + public $canEdit = false; + public $canRemove = false; + protected $corePolicyTemplates; + protected $corePolicyTemplateGroups; + /** * @return bool */ @@ -45,7 +53,16 @@ public function initialize() $this->setDefaultProperties([ 'sortAlias' => 'modAccessPolicyTemplate', 'query' => '', + 'exclude' => 'creator' ]); + $this->isGridFilter = $this->getProperty('isGridFilter', false); + + $this->canCreate = $this->modx->hasPermission('policy_template_new') && $this->modx->hasPermission('policy_template_save'); + $this->canEdit = $this->modx->hasPermission('policy_template_edit'); + $this->canRemove = $this->modx->hasPermission('policy_template_delete'); + $this->corePolicyTemplates = $this->classKey::getCoreTemplates(); + $this->corePolicyTemplateGroups = modAccessPolicyTemplateGroup::getCoreGroups(); + return $initialized; } @@ -104,22 +121,36 @@ public function prepareQueryAfterCount(xPDOQuery $c) } /** - * @param xPDOObject $object + * @param xPDOObject|modAccessPolicyTemplate $object * @return array */ public function prepareRow(xPDOObject $object) { - $template = $object->toArray(); - - $template['description_trans'] = $this->modx->lexicon($template['description']); - $template['cls'] = $this->prepareRowClasses($object); + $permissions = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + $templateData = $object->toArray(); + $templateName = $object->get('name'); + $isCoreTemplate = $object->isCoreTemplate($templateName); + + $templateData['reserved'] = ['name' => $this->corePolicyTemplates]; + $templateData['isProtected'] = $isCoreTemplate; + $templateData['creator'] = $isCoreTemplate ? 'modx' : strtolower($this->modx->lexicon('user')) ; + if ($isCoreTemplate) { + unset($permissions['delete']); + } + $templateData['permissions'] = $permissions; + $templateData['description_trans'] = $this->modx->lexicon($templateData['description']); - return $template; + return $templateData; } /** * @param xPDOObject|modAccessPolicyTemplate $object - * + * @deprecated as of 3.1 * @return string */ protected function prepareRowClasses(xPDOObject $object) diff --git a/core/src/Revolution/modAccessPolicyTemplate.php b/core/src/Revolution/modAccessPolicyTemplate.php index 426706093f..d4fc135966 100644 --- a/core/src/Revolution/modAccessPolicyTemplate.php +++ b/core/src/Revolution/modAccessPolicyTemplate.php @@ -3,30 +3,33 @@ namespace MODX\Revolution; use xPDO\Om\xPDOSimpleObject; +use xPDO\xPDO; /** * A collection of modAccessPermission records that are used as a Template for custom modAccessPolicy objects. Is * grouped into Access Policy Template Groups to provide targeted policy access implementations. * - * @property int $template_group The group that this template is a part of, used for targeting usage of applied Policies - * @property string $name The name of the Policy Template - * @property string $description A description of the Policy Template - * @property string $lexicon Optional. A lexicon that may be loaded to provide translations for all included Permissions + * @property int $template_group The group that this template is a part of, used for targeting usage of applied Policies + * @property string $name The name of the Policy Template + * @property string $description A description of the Policy Template + * @property string $lexicon Optional. A lexicon that may be loaded to provide translations for all included Permissions * * @property modAccessPermission[] $Permissions - * @property modAccessPolicy[] $Policies + * @property modAccessPolicy[] $Policies + * + * @property modX|xPDO $xpdo * * @package MODX\Revolution */ class modAccessPolicyTemplate extends xPDOSimpleObject { - const TEMPLATE_ADMINISTRATOR = 'AdministratorTemplate'; - const TEMPLATE_CONTEXT = 'ContextTemplate'; - const TEMPLATE_ELEMENT = 'ElementTemplate'; - const TEMPLATE_MEDIA_SOURCE = 'MediaSourceTemplate'; - const TEMPLATE_NAMESPACE = 'NamespaceTemplate'; - const TEMPLATE_OBJECT = 'ObjectTemplate'; - const TEMPLATE_RESOURCE = 'ResourceTemplate'; + public const TEMPLATE_ADMINISTRATOR = 'AdministratorTemplate'; + public const TEMPLATE_CONTEXT = 'ContextTemplate'; + public const TEMPLATE_ELEMENT = 'ElementTemplate'; + public const TEMPLATE_MEDIA_SOURCE = 'MediaSourceTemplate'; + public const TEMPLATE_NAMESPACE = 'NamespaceTemplate'; + public const TEMPLATE_OBJECT = 'ObjectTemplate'; + public const TEMPLATE_RESOURCE = 'ResourceTemplate'; /** * Returns list of core Policy Templates diff --git a/manager/assets/modext/widgets/security/modx.grid.access.policy.js b/manager/assets/modext/widgets/security/modx.grid.access.policy.js index 4631ca43d4..a368b6ad61 100644 --- a/manager/assets/modext/widgets/security/modx.grid.access.policy.js +++ b/manager/assets/modext/widgets/security/modx.grid.access.policy.js @@ -7,30 +7,30 @@ * @xtype modx-panel-access-policies */ MODx.panel.AccessPolicies = function(config = {}) { - Ext.applyIf(config,{ - id: 'modx-panel-access-policies' - ,bodyStyle: '' - ,defaults: { collapsible: false, autoHeight: true } - ,items: [{ - html: _('policies') - ,id: 'modx-policies-header' - ,xtype: 'modx-header' - },{ - layout: 'form' - ,cls: 'main-wrapper' - ,items: [{ - html: '

          '+_('policy_management_msg')+'

          ' - ,border: false - },{ - xtype: 'modx-grid-access-policy' - ,preventRender: true + Ext.applyIf(config, { + id: 'modx-panel-access-policies', + bodyStyle: '', + defaults: { collapsible: false, autoHeight: true }, + items: [{ + html: _('policies'), + id: 'modx-policies-header', + xtype: 'modx-header' + }, { + layout: 'form', + cls: 'main-wrapper', + items: [{ + html: `

          ${_('policy_management_msg')}

          `, + border: false + }, { + xtype: 'modx-grid-access-policy', + preventRender: true }] }] }); - MODx.panel.AccessPolicies.superclass.constructor.call(this,config); + MODx.panel.AccessPolicies.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.AccessPolicies,MODx.FormPanel); -Ext.reg('modx-panel-access-policies',MODx.panel.AccessPolicies); +Ext.extend(MODx.panel.AccessPolicies, MODx.FormPanel); +Ext.reg('modx-panel-access-policies', MODx.panel.AccessPolicies); /** * Loads a grid of modAccessPolicies. @@ -43,212 +43,252 @@ Ext.reg('modx-panel-access-policies',MODx.panel.AccessPolicies); MODx.grid.AccessPolicy = function(config = {}) { const queryValue = this.applyRequestFilter(2, 'query', 'tab', true); this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - id: 'modx-grid-access-policy' - ,url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + id: 'modx-grid-access-policy', + url: MODx.config.connector_url, + baseParams: { action: 'Security/Access/Policy/GetList' - } - ,fields: [ + }, + fields: [ 'id', 'name', 'description', 'description_trans', - 'class', - 'data', 'parent', 'template', 'template_name', 'active_permissions', 'total_permissions', 'active_of', - 'cls' - ] - ,paging: true - ,autosave: true - ,save_action: 'Security/Access/Policy/UpdateFromGrid' - ,remoteSort: true - ,sm: this.sm - ,columns: [this.sm,{ - header: _('policy_name') - ,dataIndex: 'name' - ,width: 200 - ,editor: { xtype: 'textfield' ,allowBlank: false } - ,sortable: true - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/access/policy/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 375 - ,renderer: function(value, metaData, record) { - return Ext.util.Format.htmlEncode(record['data']['description_trans']); + 'creator' + ], + paging: true, + autosave: true, + save_action: 'Security/Access/Policy/UpdateFromGrid', + remoteSort: true, + sm: this.sm, + columns: [this.sm, { + header: _('policy_name'), + dataIndex: 'name', + id: 'modx-policy--name', + width: 200, + editor: { + xtype: 'textfield', + allowBlank: false + }, + sortable: true, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=security/access/policy/update&id=${record.data.id}`, + title: _('policy_edit') + }) + : value + ; + }, + scope: this } - ,editable: false - },{ - header: _('policy_template') - ,dataIndex: 'template_name' - ,width: 375 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/access/policy/template/update&id=' + record.data.template - ,target: '_blank' - }); - }, scope: this } - },{ - header: _('active_permissions') - ,dataIndex: 'active_of' - ,width: 100 - ,editable: false - }] - ,tbar: [ + }, { + header: _('description'), + dataIndex: 'description', + id: 'modx-policy--description', + width: 375, + editor: { + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return record.json.description_trans || value; + }, + scope: this + } + }, { + header: _('policy_template'), + dataIndex: 'template_name', + width: 375, + renderer: { + fn: function(value, metaData, record) { + const objPermissions = record.json.permissions; + return !Ext.isEmpty(objPermissions) && objPermissions.updateTemplate === true + ? this.renderLink(value, { + href: `?a=security/access/policy/template/update&id=${record.data.template}`, + title: _('policy_template_edit'), + target: '_blank' + }) + : value + ; + }, + scope: this + } + }, + this.getCreatorColumnConfig('policy'), + { + header: _('active_permissions'), + dataIndex: 'active_of', + width: 100, + editable: false + }], + tbar: [ { - text: _('create') - ,cls:'primary-button' - ,scope: this - ,handler: this.createPolicy - },{ - text: _('import') - ,scope: this - ,handler: this.importPolicy - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this - }] + text: _('create'), + cls: 'primary-button', + scope: this, + handler: this.createPolicy + }, { + text: _('import'), + scope: this, + handler: this.importPolicy }, + this.getBulkActionsButton('policy', 'Security/Access/Policy/RemoveMultiple'), '->', this.getQueryFilterField(`filter-query-policy:${queryValue}`), this.getClearFiltersButton('filter-query-policy') - ] + ], + viewConfig: this.getViewConfig() + }); + MODx.grid.AccessPolicy.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate', 'export']; + + this.setUserCanEdit(['policy_save', 'policy_edit']); + this.setUserCanCreate(['policy_save', 'policy_new']); + this.setUserCanDelete(['policy_delete']); + this.setShowActionsMenu(); + + this.on({ + render: function(grid) { + this.setEditableColumnAccess( + ['modx-policy--name', 'modx-policy--description'] + ); + }, + beforeedit: function(e) { + if (e.record.json.isProtected || !this.userCanEditRecord(e.record)) { + return false; + } + }, + afteredit: function(e) { + this.refresh(); + } }); - MODx.grid.AccessPolicy.superclass.constructor.call(this,config); }; -Ext.extend(MODx.grid.AccessPolicy,MODx.grid.Grid,{ - editPolicy: function(itm,e) { - MODx.loadPage('security/access/policy/update', 'id='+this.menu.record.id); - } +Ext.extend(MODx.grid.AccessPolicy, MODx.grid.Grid, { + getMenu: function() { + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (this.getSelectionModel().getCount() > 1) { + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected + }); + } else { + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.editPolicy + }); + } + if (this.userCanCreate && this.userCanDuplicateRecord(record)) { + menu.push({ + text: _('duplicate'), + handler: this.confirm.createDelegate(this, ['Security/Access/Policy/Duplicate', 'policy_duplicate_confirm']) + }); + } + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('export'), + handler: this.exportPolicy + }); + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('delete'), + handler: this.confirm.createDelegate(this, ['Security/Access/Policy/Remove', 'policy_remove_confirm']) + }); + } + } + + if (menu.length > 0) { + this.addContextMenuItem(menu); + } + }, + + editPolicy: function(itm, e) { + MODx.loadPage('security/access/policy/update', `id=${this.menu.record.id}`); + }, - ,createPolicy: function(btn,e) { - var r = this.menu.record; + createPolicy: function(btn, e) { + const { record } = this.menu; if (!this.windows.apc) { this.windows.apc = MODx.load({ - xtype: 'modx-window-access-policy-create' - ,record: r - ,plugin: this.config.plugin - ,listeners: { - 'success': {fn:function(r) { - this.refresh(); - },scope:this} + xtype: 'modx-window-access-policy-create', + record: record, + plugin: this.config.plugin, + listeners: { + success: { + fn: function() { + this.refresh(); + }, + scope: this + } } }); } this.windows.apc.reset(); this.windows.apc.show(e.target); - } + }, - ,exportPolicy: function(btn,e) { - var id = this.menu.record.id; + exportPolicy: function(btn, e) { + const { id } = this.menu.record; MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Access/Policy/Export' - ,id: id - } - ,listeners: { - 'success': {fn:function(r) { - location.href = this.config.url+'?action=Security/Access/Policy/Export&download=1&id='+id+'&HTTP_MODAUTH='+MODx.siteId; - },scope:this} + url: this.config.url, + params: { + action: 'Security/Access/Policy/Export', + id: id + }, + listeners: { + success: { + fn: function(r) { + window.location.href = `${this.config.url}?action=Security/Access/Policy/Export&download=1&id=${id}&HTTP_MODAUTH=${MODx.siteId}`; + }, + scope: this + } } }); - } + }, - ,importPolicy: function(btn,e) { - var r = {}; + importPolicy: function(btn, e) { + const record = {}; if (!this.windows.importPolicy) { this.windows.importPolicy = MODx.load({ - xtype: 'modx-window-policy-import' - ,record: r - ,listeners: { - 'success': {fn:function(o) { - this.refresh(); - },scope:this} + xtype: 'modx-window-policy-import', + record: record, + listeners: { + success: { + fn: function(o) { + this.refresh(); + }, + scope: this + } } }); } this.windows.importPolicy.reset(); - this.windows.importPolicy.setValues(r); + this.windows.importPolicy.setValues(record); this.windows.importPolicy.show(e.target); } - - ,getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; - if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected - }); - } else { - if (p.indexOf('pedit') != -1) { - m.push({ - text: _('edit') - ,handler: this.editPolicy - }); - m.push({ - text: _('duplicate') - ,handler: this.confirm.createDelegate(this,["Security/Access/Policy/Duplicate","policy_duplicate_confirm"]) - }); - } - if (m.length > 0) { m.push('-'); } - m.push({ - text: _('export') - ,handler: this.exportPolicy - }); - if (p.indexOf('premove') != -1) { - if (m.length > 0) m.push('-'); - m.push({ - text: _('delete') - ,handler: this.confirm.createDelegate(this,["Security/Access/Policy/Remove","policy_remove_confirm"]) - }); - } - } - - if (m.length > 0) { - this.addContextMenuItem(m); - } - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; - - MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('policy_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Security/Access/Policy/RemoveMultiple' - ,policies: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} - } - }); - return true; - } }); -Ext.reg('modx-grid-access-policy',MODx.grid.AccessPolicy); +Ext.reg('modx-grid-access-policy', MODx.grid.AccessPolicy); /** * Generates a window for creating Access Policies. @@ -259,65 +299,60 @@ Ext.reg('modx-grid-access-policy',MODx.grid.AccessPolicy); * @xtype modx-window-access-policy-create */ MODx.window.CreateAccessPolicy = function(config = {}) { - this.ident = config.ident || 'cacp'+Ext.id(); - Ext.applyIf(config,{ - title: _('create') - ,url: MODx.config.connector_url - ,action: 'Security/Access/Policy/Create' - ,fields: [{ - fieldLabel: _('name') - ,description: MODx.expandHelp ? '' : _('policy_desc_name') - ,name: 'name' - ,id: 'modx-'+this.ident+'-name' - ,xtype: 'textfield' - ,allowBlank: false - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-name' - ,html: _('policy_desc_name') - ,cls: 'desc-under' - },{ - fieldLabel: _('policy_template') - ,description: MODx.expandHelp ? '' : _('policy_desc_template') - ,name: 'template' - ,hiddenName: 'template' - ,id: 'modx-'+this.ident+'-template' - ,xtype: 'modx-combo-access-policy-template' - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-template' - ,html: _('policy_desc_template') - ,cls: 'desc-under' - },{ - fieldLabel: _('description') - ,description: MODx.expandHelp ? '' : _('policy_desc_description') - ,name: 'description' - ,id: 'modx-'+this.ident+'-description' - ,xtype: 'textarea' - ,anchor: '100%' - ,height: 50 - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-description' - ,html: _('policy_desc_description') - ,cls: 'desc-under' - },{ - name: 'class' - ,id: 'modx-'+this.ident+'-class' - ,xtype: 'hidden' - },{ - name: 'id' - ,id: 'modx-'+this.ident+'-id' - ,xtype: 'hidden' - }] - ,keys: [] + this.ident = config.ident || `window--create-policy-${Ext.id()}`; + Ext.applyIf(config, { + title: _('create'), + url: MODx.config.connector_url, + action: 'Security/Access/Policy/Create', + fields: [{ + fieldLabel: _('name'), + description: MODx.expandHelp ? '' : _('policy_desc_name'), + name: 'name', + xtype: 'textfield', + allowBlank: false, + anchor: '100%' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('policy_desc_name'), + cls: 'desc-under' + }, { + fieldLabel: _('policy_template'), + description: MODx.expandHelp ? '' : _('policy_desc_template'), + name: 'template', + hiddenName: 'template', + xtype: 'modx-combo-access-policy-template', + anchor: '100%' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('policy_desc_template'), + cls: 'desc-under' + }, { + fieldLabel: _('description'), + description: MODx.expandHelp ? '' : _('policy_desc_description'), + name: 'description', + xtype: 'textarea', + anchor: '100%', + height: 50 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('policy_desc_description'), + cls: 'desc-under' + }, { + name: 'class', + xtype: 'hidden' + }, { + name: 'id', + xtype: 'hidden' + }], + keys: [] }); - MODx.window.CreateAccessPolicy.superclass.constructor.call(this,config); + MODx.window.CreateAccessPolicy.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateAccessPolicy,MODx.Window); -Ext.reg('modx-window-access-policy-create',MODx.window.CreateAccessPolicy); +Ext.extend(MODx.window.CreateAccessPolicy, MODx.Window); +Ext.reg('modx-window-access-policy-create', MODx.window.CreateAccessPolicy); /** * @class MODx.window.AccessPolicyTemplate @@ -326,26 +361,37 @@ Ext.reg('modx-window-access-policy-create',MODx.window.CreateAccessPolicy); * @xtype modx-combo-access-policy-template */ MODx.combo.AccessPolicyTemplate = function(config = {}) { - Ext.applyIf(config,{ - name: 'template' - ,hiddenName: 'template' - ,fields: ['id','name','description','description_trans'] - ,forceSelection: true - ,typeAhead: false - ,editable: false - ,allowBlank: false - ,pageSize: 20 - ,url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + name: 'template', + hiddenName: 'template', + fields: [ + 'id', + 'name', + 'description', + 'description_trans' + ], + forceSelection: true, + typeAhead: false, + editable: false, + allowBlank: false, + pageSize: 20, + url: MODx.config.connector_url, + baseParams: { action: 'Security/Access/Policy/Template/GetList' - } - ,tpl: new Ext.XTemplate('
          {name:htmlEncode}' - ,'

          {description_trans:htmlEncode}

          ') + }, + tpl: new Ext.XTemplate(` + +
          + {name:htmlEncode} +

          {description_trans:htmlEncode}

          +
          +
          + `) }); - MODx.combo.AccessPolicyTemplate.superclass.constructor.call(this,config); + MODx.combo.AccessPolicyTemplate.superclass.constructor.call(this, config); }; -Ext.extend(MODx.combo.AccessPolicyTemplate,MODx.combo.ComboBox); -Ext.reg('modx-combo-access-policy-template',MODx.combo.AccessPolicyTemplate); +Ext.extend(MODx.combo.AccessPolicyTemplate, MODx.combo.ComboBox); +Ext.reg('modx-combo-access-policy-template', MODx.combo.AccessPolicyTemplate); /** * @class MODx.window.ImportPolicy @@ -354,29 +400,27 @@ Ext.reg('modx-combo-access-policy-template',MODx.combo.AccessPolicyTemplate); * @xtype modx-window-policy-import */ MODx.window.ImportPolicy = function(config = {}) { - this.ident = config.ident || 'imppol-'+Ext.id(); - Ext.applyIf(config,{ - title: _('import') - ,id: 'modx-window-policy-import' - ,url: MODx.config.connector_url - ,action: 'Security/Access/Policy/Import' - ,fileUpload: true - ,saveBtnText: _('import') - ,fields: [{ - html: _('policy_import_msg') - ,id: this.ident+'-desc' - ,xtype: 'modx-description' - ,style: 'margin-bottom: 10px;' - },{ - xtype: 'fileuploadfield' - ,fieldLabel: _('file') - ,buttonText: _('upload.buttons.upload') - ,name: 'file' - ,id: this.ident+'-file' - ,anchor: '100%' + this.ident = config.ident || `window--import-policy-${Ext.id()}`; + Ext.applyIf(config, { + title: _('import'), + id: 'modx-window-policy-import', + url: MODx.config.connector_url, + action: 'Security/Access/Policy/Import', + fileUpload: true, + saveBtnText: _('import'), + fields: [{ + html: _('policy_import_msg'), + xtype: 'modx-description', + style: 'margin-bottom: 10px;' + }, { + xtype: 'fileuploadfield', + fieldLabel: _('file'), + buttonText: _('upload.buttons.upload'), + name: 'file', + anchor: '100%' }] }); - MODx.window.ImportPolicy.superclass.constructor.call(this,config); + MODx.window.ImportPolicy.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.ImportPolicy,MODx.Window); -Ext.reg('modx-window-policy-import',MODx.window.ImportPolicy); +Ext.extend(MODx.window.ImportPolicy, MODx.Window); +Ext.reg('modx-window-policy-import', MODx.window.ImportPolicy); diff --git a/manager/assets/modext/widgets/security/modx.grid.access.policy.template.js b/manager/assets/modext/widgets/security/modx.grid.access.policy.template.js index 8fee8c4f59..32d4f45bad 100644 --- a/manager/assets/modext/widgets/security/modx.grid.access.policy.template.js +++ b/manager/assets/modext/widgets/security/modx.grid.access.policy.template.js @@ -7,30 +7,30 @@ * @xtype modx-panel-access-policy-templates */ MODx.panel.AccessPolicyTemplates = function(config = {}) { - Ext.applyIf(config,{ - id: 'modx-panel-access-policy-templates' - ,bodyStyle: '' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('policies') - ,id: 'modx-policy-templates-header' - ,xtype: 'modx-header' - },{ - layout: 'form' - ,bodyStyle: 'padding: 15px' - ,items: [{ - html: '

          '+_('policy_templates.intro_msg')+'

          ' - ,border: false - },{ - xtype: 'modx-grid-access-policy-templates' - ,preventRender: true + Ext.applyIf(config, { + id: 'modx-panel-access-policy-templates', + bodyStyle: '', + defaults: { collapsible: false, autoHeight: true }, + items: [{ + html: _('policies'), + id: 'modx-policy-templates-header', + xtype: 'modx-header' + }, { + layout: 'form', + bodyStyle: 'padding: 15px', + items: [{ + html: `

          ${_('policy_templates.intro_msg')}

          `, + border: false + }, { + xtype: 'modx-grid-access-policy-templates', + preventRender: true }] }] }); - MODx.panel.AccessPolicyTemplates.superclass.constructor.call(this,config); + MODx.panel.AccessPolicyTemplates.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.AccessPolicyTemplates,MODx.FormPanel); -Ext.reg('modx-panel-access-policy-templates',MODx.panel.AccessPolicyTemplates); +Ext.extend(MODx.panel.AccessPolicyTemplates, MODx.FormPanel); +Ext.reg('modx-panel-access-policy-templates', MODx.panel.AccessPolicyTemplates); /** * Loads a grid of modAccessPolicyTemplates. @@ -43,13 +43,13 @@ Ext.reg('modx-panel-access-policy-templates',MODx.panel.AccessPolicyTemplates); MODx.grid.AccessPolicyTemplate = function(config = {}) { const queryValue = this.applyRequestFilter(3, 'query', 'tab', true); this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - id: 'modx-grid-access-policy-template' - ,url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + id: 'modx-grid-access-policy-template', + url: MODx.config.connector_url, + baseParams: { action: 'Security/Access/Policy/Template/GetList' - } - ,fields: [ + }, + fields: [ 'id', 'name', 'description', @@ -58,214 +58,337 @@ MODx.grid.AccessPolicyTemplate = function(config = {}) { 'template_group_name', 'total_permissions', 'policy_count', - 'cls' - ] - ,paging: true - ,autosave: true - ,save_action: 'Security/Access/Policy/Template/UpdateFromGrid' - ,remoteSort: true - ,sm: this.sm - ,columns: [this.sm,{ - header: _('name') - ,dataIndex: 'name' - ,width: 200 - ,editor: { xtype: 'textfield' ,allowBlank: false } - ,sortable: true - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/access/policy/template/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 375 - ,editable: false - ,renderer: function(value, metaData, record) { - return Ext.util.Format.htmlEncode(record['data']['description_trans']); + 'creator' + ], + paging: true, + autosave: true, + save_action: 'Security/Access/Policy/Template/UpdateFromGrid', + remoteSort: true, + sm: this.sm, + columns: [this.sm, { + header: _('name'), + dataIndex: 'name', + id: 'modx-policy-template--name', + width: 200, + editor: { + xtype: 'textfield', + allowBlank: false + }, + sortable: true, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=security/access/policy/template/update&id=${record.data.id}`, + title: _('policy_template_edit') + }) + : value + ; + }, + scope: this } - ,sortable: true - },{ - header: _('template_group') - ,dataIndex: 'template_group_name' - ,width: 375 - ,sortable: true - },{ - header: _('policy_count') - ,dataIndex: 'policy_count' - ,width: 100 - ,editable: false - ,sortable: true - },{ - header: _('permissions') - ,dataIndex: 'total_permissions' - ,width: 100 - ,editable: false - ,sortable: true - }] - ,tbar: [ + }, { + header: _('description'), + dataIndex: 'description', + id: 'modx-policy-template--description', + width: 375, + editor: { + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return record.json.description_trans || value; + }, + scope: this + } + }, { + header: _('template_group'), + dataIndex: 'template_group_name', + width: 375, + sortable: true + }, { + header: _('policy_count'), + dataIndex: 'policy_count', + width: 100, + editable: false, + sortable: true + }, { + header: _('permissions'), + dataIndex: 'total_permissions', + width: 100, + editable: false, + sortable: true + }, + this.getCreatorColumnConfig('policy-template') + ], + tbar: [ { - text: _('create') - ,cls:'primary-button' - ,scope: this - ,handler: this.createPolicyTemplate - },{ - text: _('import') - ,scope: this - ,handler: this.importPolicyTemplate - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this - }] + text: _('create'), + cls: 'primary-button', + scope: this, + handler: this.createPolicyTemplate + }, { + text: _('import'), + scope: this, + handler: this.importPolicyTemplate + }, { + /* + * Note: Using local this.removeSelected method instead of shared base this.getBulkActionsButton() method here, + * as additional validation processing is needed for removal of Policy Templates + */ + text: _('bulk_actions'), + menu: [{ + text: _('selected_remove'), + itemId: 'modx-bulk-menu-opt-remove', + handler: this.removeSelected, + scope: this + }], + listeners: { + render: { + fn: function(btn) { + if (!this.userCanDelete) { + btn.hide(); + } + }, + scope: this + }, + click: { + fn: function(btn) { + const + removableItems = this.getRemovableItemsFromSelection('int'), + menuOptRemove = btn.menu.getComponent('modx-bulk-menu-opt-remove') + ; + if (removableItems.length === 0) { + menuOptRemove.disable(); + } else { + menuOptRemove.enable(); + } + }, + scope: this + } + } }, '->', this.getQueryFilterField(`filter-query-policy-template:${queryValue}`), this.getClearFiltersButton('filter-query-policy-template') - ] + ], + viewConfig: this.getViewConfig() + }); + MODx.grid.AccessPolicyTemplate.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate', 'export']; + + this.setUserCanEdit(['policy_template_save', 'policy_template_edit']); + this.setUserCanCreate(['policy_template_save', 'policy_template_new']); + this.setUserCanDelete(['policy_template_delete']); + this.setShowActionsMenu(); + + this.on({ + render: function(grid) { + this.setEditableColumnAccess( + ['modx-policy-template--name', 'modx-policy-template--description'] + ); + }, + beforeedit: function(e) { + if (e.record.json.isProtected || !this.userCanEditRecord(e.record)) { + return false; + } + }, + afteredit: function(e) { + this.refresh(); + } }); - MODx.grid.AccessPolicyTemplate.superclass.constructor.call(this,config); }; -Ext.extend(MODx.grid.AccessPolicyTemplate,MODx.grid.Grid,{ +Ext.extend(MODx.grid.AccessPolicyTemplate, MODx.grid.Grid, { getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected }); } else { - if (p.indexOf('pedit') !== -1) { - m.push({ - text: _('edit') - ,handler: this.editPolicyTemplate + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.editPolicyTemplate }); - m.push({ - text: _('duplicate') - ,handler: this.confirm.createDelegate(this,["Security/Access/Policy/Template/Duplicate","policy_template_duplicate_confirm"]) + } + if (this.userCanCreate && this.userCanDuplicateRecord(record)) { + menu.push({ + text: _('duplicate'), + handler: this.confirm.createDelegate( + this, + [ + 'Security/Access/Policy/Template/Duplicate', + 'policy_template_duplicate_confirm' + ] + ) }); } - if (m.length > 0) { m.push('-'); } - m.push({ - text: _('export') - ,handler: this.exportPolicyTemplate + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('export'), + handler: this.exportPolicyTemplate }); - - if (p.indexOf('premove') !== -1) { - if (m.length > 0) m.push('-'); - m.push({ + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + if (menu.length > 0) { + menu.push('-'); + } + /* + * Note: Using local this.removePolicyTemplate method instead of shared base this.remove() method here, + * as additional validation processing is needed for removal of Policy Templates + */ + menu.push({ text: _('delete'), handler: this.removePolicyTemplate }); } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (menu.length > 0) { + this.addContextMenuItem(menu); } - } + }, - ,createPolicyTemplate: function(btn,e) { - var r = this.menu.record; - if (!this.windows.aptc) { - this.windows.aptc = MODx.load({ - xtype: 'modx-window-access-policy-template-create' - ,record: r - ,plugin: this.config.plugin - ,listeners: { - 'success': {fn:function(r) { - this.refresh(); - },scope:this} + createPolicyTemplate: function(btn, e) { + const { record } = this.menu; + if (!this.windows.create_policy_template) { + this.windows.create_policy_template = MODx.load({ + xtype: 'modx-window-access-policy-template-create', + record: record, + plugin: this.config.plugin, + listeners: { + success: { + fn: function(response) { + this.refresh(); + }, + scope: this + } } }); } - this.windows.aptc.reset(); - this.windows.aptc.show(e.target); - } + this.windows.create_policy_template.reset(); + this.windows.create_policy_template.show(e.target); + }, - ,importPolicyTemplate: function(btn,e) { - var r = {}; + importPolicyTemplate: function(btn, e) { + const record = {}; if (!this.windows.importPolicyTemplate) { this.windows.importPolicyTemplate = MODx.load({ - xtype: 'modx-window-policy-template-import' - ,record: r - ,listeners: { - 'success': {fn:function(o) { - this.refresh(); - },scope:this} + xtype: 'modx-window-policy-template-import', + record: record, + listeners: { + success: { + fn: function(response) { + this.refresh(); + }, + scope: this + } } }); } this.windows.importPolicyTemplate.reset(); - this.windows.importPolicyTemplate.setValues(r); + this.windows.importPolicyTemplate.setValues(record); this.windows.importPolicyTemplate.show(e.target); - } + }, - ,exportPolicyTemplate: function(btn,e) { - var id = this.menu.record.id; + exportPolicyTemplate: function(btn, e) { + const { id } = this.menu.record; MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Access/Policy/Template/Export' - ,id: id - } - ,listeners: { - 'success': {fn:function(r) { - location.href = this.config.url+'?action=Security/Access/Policy/Template/Export&download=1&id='+id+'&HTTP_MODAUTH='+MODx.siteId; - },scope:this} + url: this.config.url, + params: { + action: 'Security/Access/Policy/Template/Export', + id: id + }, + listeners: { + success: { + fn: function(r) { + window.location.href = `${this.config.url}?action=Security/Access/Policy/Template/Export&download=1&id=${id}&HTTP_MODAUTH=${MODx.siteId}`; + }, + scope: this + } } }); - } + }, - ,editPolicyTemplate: function(itm,e) { - MODx.loadPage('security/access/policy/template/update', 'id='+this.menu.record.id); - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + editPolicyTemplate: function(itm, e) { + MODx.loadPage('security/access/policy/template/update', `id=${this.menu.record.id}`); + }, - var store = this.getStore(); - var policiesCount = 0; - cs.split(',').forEach(function(item){ + removeSelected: function() { + const selectedTemplates = this.getSelectedAsList(); + if (selectedTemplates === false) { + return false; + } + const + store = this.getStore(), + selectedTemplatesArr = selectedTemplates.split(','), + totalSelected = selectedTemplatesArr.length + ; + let + policiesCount = 0, + selectionsProtected = 0, + confirmationMessage + ; + selectedTemplatesArr.forEach(item => { const record = store.getById(item); - if (record) { - policiesCount += parseInt(record.data.policy_count); + if (!record.json.isProtected) { + policiesCount += parseInt(record.data.policy_count, 10); + } else { + selectionsProtected++; + } } - - }) - + }); + if (policiesCount) { + confirmationMessage = selectionsProtected > 0 + ? _('policy_template_remove_multiple_confirm_in_use_ignoring_protected', { 'count-policies': policiesCount, protected: selectionsProtected, 'count-templates': totalSelected }) + : _('policy_template_remove_multiple_confirm_in_use', { count: policiesCount, total: totalSelected }) + ; + } else { + confirmationMessage = _('policy_template_remove_multiple_confirm'); + } MODx.msg.confirm({ - title: _('selected_remove') - ,text: policiesCount ? _('policy_template_remove_multiple_confirm_in_use', {count: policiesCount}) : _('policy_template_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Security/Access/Policy/Template/RemoveMultiple' - ,templates: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + title: _('selected_remove'), + text: confirmationMessage, + url: this.config.url, + params: { + action: 'Security/Access/Policy/Template/RemoveMultiple', + templates: selectedTemplates + }, + listeners: { + success: { + fn: function(response) { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,removePolicyTemplate: function() { - if (!this.menu.record) return; + }, + removePolicyTemplate: function() { + if (!this.menu.record) { + return; + } MODx.msg.confirm({ title: _('warning'), - text: parseInt(this.menu.record.policy_count) ? _('policy_template_remove_confirm_in_use', {count: this.menu.record.policy_count}) : _('policy_template_remove_confirm'), + text: parseInt(this.menu.record.policy_count, 10) + ? _('policy_template_remove_confirm_in_use', { count: this.menu.record.policy_count }) + : _('policy_template_remove_confirm'), url: this.config.url, params: { action: 'Security/Access/Policy/Template/Remove', @@ -274,13 +397,13 @@ Ext.extend(MODx.grid.AccessPolicyTemplate,MODx.grid.Grid,{ listeners: { success: { fn: this.refresh, - scope:this + scope: this } } }); } }); -Ext.reg('modx-grid-access-policy-templates',MODx.grid.AccessPolicyTemplate); +Ext.reg('modx-grid-access-policy-templates', MODx.grid.AccessPolicyTemplate); /** * Generates a window for creating Access Policies. @@ -291,53 +414,50 @@ Ext.reg('modx-grid-access-policy-templates',MODx.grid.AccessPolicyTemplate); * @xtype modx-window-access-policy-create */ MODx.window.CreateAccessPolicyTemplate = function(config = {}) { - this.ident = config.ident || 'cacpt'+Ext.id(); - Ext.applyIf(config,{ - title: _('create') - ,url: MODx.config.connector_url - ,action: 'Security/Access/Policy/Template/Create' - ,fields: [{ - fieldLabel: _('name') - ,name: 'name' - ,id: 'modx-'+this.ident+'-name' - ,xtype: 'textfield' - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-name' - ,html: _('policy_template_desc_name') - ,cls: 'desc-under' - },{ - fieldLabel: _('template_group') - ,name: 'template_group' - ,id: 'modx-'+this.ident+'-template-group' - ,xtype: 'modx-combo-access-policy-template-group' - ,anchor: '100%' - ,value: 1 - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-template-group' - ,html: _('policy_template_desc_template_group') - ,cls: 'desc-under' - },{ - fieldLabel: _('description') - ,name: 'description' - ,id: 'modx-'+this.ident+'-description' - ,xtype: 'textarea' - ,anchor: '100%' - ,height: 50 - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-description' - ,html: _('policy_template_desc_description') - ,cls: 'desc-under' - }] - ,keys: [] + this.ident = config.ident || `window-import-policy-template-${Ext.id()}`; + Ext.applyIf(config, { + title: _('create'), + url: MODx.config.connector_url, + action: 'Security/Access/Policy/Template/Create', + fields: [{ + fieldLabel: _('name'), + name: 'name', + xtype: 'textfield', + anchor: '100%' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('policy_template_desc_name'), + cls: 'desc-under' + }, { + fieldLabel: _('template_group'), + name: 'template_group', + xtype: 'modx-combo-access-policy-template-group', + anchor: '100%', + value: 1 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('policy_template_desc_template_group'), + cls: 'desc-under' + }, { + fieldLabel: _('description'), + name: 'description', + xtype: 'textarea', + anchor: '100%', + height: 50 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('policy_template_desc_description'), + cls: 'desc-under' + }], + keys: [] }); - MODx.window.CreateAccessPolicyTemplate.superclass.constructor.call(this,config); + MODx.window.CreateAccessPolicyTemplate.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateAccessPolicyTemplate,MODx.Window); -Ext.reg('modx-window-access-policy-template-create',MODx.window.CreateAccessPolicyTemplate); +Ext.extend(MODx.window.CreateAccessPolicyTemplate, MODx.Window); +Ext.reg('modx-window-access-policy-template-create', MODx.window.CreateAccessPolicyTemplate); /** * @class MODx.window.ImportPolicyTemplate @@ -346,29 +466,27 @@ Ext.reg('modx-window-access-policy-template-create',MODx.window.CreateAccessPoli * @xtype modx-window-policy-template-import */ MODx.window.ImportPolicyTemplate = function(config = {}) { - this.ident = config.ident || 'imppt-'+Ext.id(); - Ext.applyIf(config,{ - title: _('import') - ,id: 'modx-window-policy-template-import' - ,url: MODx.config.connector_url - ,action: 'Security/Access/Policy/Template/Import' - ,fileUpload: true - ,saveBtnText: _('import') - ,fields: [{ - html: _('policy_template_import_msg') - ,id: this.ident+'-desc' - ,xtype: 'modx-description' - ,style: 'margin-bottom: 10px;' - },{ - xtype: 'fileuploadfield' - ,fieldLabel: _('file') - ,buttonText: _('upload.buttons.upload') - ,name: 'file' - ,id: this.ident+'-file' - ,anchor: '100%' + this.ident = config.ident || `window-import-policy-template-${Ext.id()}`; + Ext.applyIf(config, { + title: _('import'), + id: 'modx-window-policy-template-import', + url: MODx.config.connector_url, + action: 'Security/Access/Policy/Template/Import', + fileUpload: true, + saveBtnText: _('import'), + fields: [{ + html: _('policy_template_import_msg'), + xtype: 'modx-description', + style: 'margin-bottom: 10px;' + }, { + xtype: 'fileuploadfield', + fieldLabel: _('file'), + buttonText: _('upload.buttons.upload'), + name: 'file', + anchor: '100%' }] }); - MODx.window.ImportPolicyTemplate.superclass.constructor.call(this,config); + MODx.window.ImportPolicyTemplate.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.ImportPolicyTemplate,MODx.Window); -Ext.reg('modx-window-policy-template-import',MODx.window.ImportPolicyTemplate); +Ext.extend(MODx.window.ImportPolicyTemplate, MODx.Window); +Ext.reg('modx-window-policy-template-import', MODx.window.ImportPolicyTemplate); From 7c2aedece0517518fff01ff1d4b37363d9fbcffc Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sun, 3 Nov 2024 22:48:18 -0500 Subject: [PATCH 10/39] Package Versions updates Formatting, code style changes only --- .../Workspace/Packages/Version/GetList.php | 22 +- .../package/package.versions.grid.js | 248 ++++++++++-------- 2 files changed, 155 insertions(+), 115 deletions(-) diff --git a/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php b/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php index d8d332e233..3f2609b2c3 100644 --- a/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php +++ b/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php @@ -1,4 +1,5 @@ urldecode($this->getProperty('package_name', $signatureArray[0])), ]; $limit = $this->getProperty('limit'); - $pkgList = $this->modx->call(modTransportPackage::class, 'listPackageVersions', - [&$this->modx, $criteria, $limit > 0 ? $limit : 0, $this->getProperty('start')]); + $pkgList = $this->modx->call( + modTransportPackage::class, + 'listPackageVersions', + [&$this->modx, $criteria, $limit > 0 ? $limit : 0, $this->getProperty('start')] + ); $data['results'] = $pkgList['collection']; $data['total'] = $pkgList['total']; return $data; @@ -137,8 +141,11 @@ public function getMetaData(modTransportPackage $package, array $packageArray) if (!empty($metadata)) { foreach ($metadata as $row) { if (!empty($row['name']) && $row['name'] === 'description') { - $packageArray['readme'] = str_replace([PHP_EOL, '

          '], ['', '
          '], - nl2br($row['text'])); + $packageArray['readme'] = str_replace( + [PHP_EOL, '

          '], + ['', '
          '], + nl2br($row['text']) + ); break; } } @@ -147,8 +154,11 @@ public function getMetaData(modTransportPackage $package, array $packageArray) $transport = $package->getTransport(); if ($transport) { $packageArray['readme'] = $transport->getAttribute('readme'); - $packageArray['readme'] = str_replace([PHP_EOL, '

          '], ['', '
          '], - nl2br($packageArray['readme'])); + $packageArray['readme'] = str_replace( + [PHP_EOL, '

          '], + ['', '
          '], + nl2br($packageArray['readme']) + ); } } unset($packageArray['attributes'], $packageArray['metadata'], $packageArray['manifest']); diff --git a/manager/assets/modext/workspace/package/package.versions.grid.js b/manager/assets/modext/workspace/package/package.versions.grid.js index 0223981dd6..b7a6668d23 100644 --- a/manager/assets/modext/workspace/package/package.versions.grid.js +++ b/manager/assets/modext/workspace/package/package.versions.grid.js @@ -1,48 +1,70 @@ -MODx.grid.PackageVersions = function(config) { - config = config || {}; +MODx.grid.PackageVersions = function(config = {}) { this.exp = new Ext.grid.RowExpander({ - tpl : new Ext.Template( + tpl: new Ext.Template( '

          {readme}

          ' ) }); - Ext.applyIf(config,{ - title: _('packages') - ,id: 'modx-grid-package-versions' - ,url: MODx.config.connector_url - ,baseParams: { - action: 'Workspace/Packages/Version/GetList' - ,signature: config.signature - ,package_name: MODx.request.package_name - } - ,fields: ['signature','name','version','release','created','updated','installed','state' - ,'workspace','provider','provider_name','disabled','source' - ,'readme','menu'] - ,plugins: [this.exp] - ,pageSize: 20 - ,columns: [this.exp,{ - header: _('name') ,dataIndex: 'name' } - ,{ header: _('version') ,dataIndex: 'version' } - ,{ header: _('release') ,dataIndex: 'release' } - ,{ header: _('installed') ,dataIndex: 'installed' ,renderer: this._rins } - ,{ - header: _('provider') - ,dataIndex: 'provider_name' - ,editable: false - }] - ,primaryKey: 'signature' - ,paging: true - ,autosave: true - ,tbar: [{ - text: _('package_versions_purge') - ,handler: this.purgePackageVersions + Ext.applyIf(config, { + title: _('packages'), + id: 'modx-grid-package-versions', + url: MODx.config.connector_url, + baseParams: { + action: 'Workspace/Packages/Version/GetList', + signature: config.signature, + package_name: MODx.request.package_name + }, + fields: [ + 'signature', + 'name', + 'version', + 'release', + 'created', + 'updated', + 'installed', + 'state', + 'workspace', + 'provider', + 'provider_name', + 'disabled', + 'source', + 'readme', + 'menu' + ], + plugins: [this.exp], + pageSize: 20, + columns: [this.exp, + { + header: _('name'), + dataIndex: 'name' + }, { + header: _('version'), + dataIndex: 'version' + }, { + header: _('release'), + dataIndex: 'release' + }, { + header: _('installed'), + dataIndex: 'installed', + renderer: this._rins + }, { + header: _('provider'), + dataIndex: 'provider_name', + editable: false + }], + primaryKey: 'signature', + paging: true, + autosave: true, + tbar: [{ + text: _('package_versions_purge'), + handler: this.purgePackageVersions }] }); - MODx.grid.PackageVersions.superclass.constructor.call(this,config); + MODx.grid.PackageVersions.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.PackageVersions,MODx.grid.Grid,{ +Ext.extend(MODx.grid.PackageVersions, MODx.grid.Grid, { - _rins: function(d,c) { - switch(d) { + _rins: function(d, c) { + switch (d) { case '': case null: c.css = 'not-installed'; @@ -51,62 +73,68 @@ Ext.extend(MODx.grid.PackageVersions,MODx.grid.Grid,{ c.css = ''; return d; } - } + }, - ,removePriorVersion: function(btn,e) { - var r = this.menu.record; + removePriorVersion: function(btn, e) { + const { record } = this.menu; MODx.msg.confirm({ - title: _('package_version_remove') - ,text: _('package_version_remove_confirm') - ,url: this.config.url - ,params: { - action: 'Workspace/Packages/Version/Remove' - ,signature: r.signature - } - ,listeners: { - 'success': {fn:function() { - if (this.fireEvent('afterRemoveRow',r)) { - this.removeActiveRow(r); - } - },scope:this} + title: _('package_version_remove'), + text: _('package_version_remove_confirm'), + url: this.config.url, + params: { + action: 'Workspace/Packages/Version/Remove', + signature: record.signature + }, + listeners: { + success: { + fn: function() { + if (this.fireEvent('afterRemoveRow', record)) { + this.removeActiveRow(record); + } + }, + scope: this + } } }); - } + }, /* Purge old package versions */ - ,purgePackageVersions: function(btn,e) { - var topic = '/Workspace/Packages/Purge/'; + purgePackageVersions: function(btn, e) { + const topic = '/Workspace/Packages/Purge/'; - this.loadWindow(btn,e,{ - xtype: 'modx-window-package-versions-purge' - ,record: { - packagename: this.config.package_name - ,topic: topic - ,register: 'mgr' - } - ,listeners: { - success: {fn: function(o) { - this.refresh(); - },scope:this} + this.loadWindow(btn, e, { + xtype: 'modx-window-package-versions-purge', + record: { + packagename: this.config.package_name, + topic: topic, + register: 'mgr' + }, + listeners: { + success: { + fn: function(o) { + this.refresh(); + }, + scope: this + } } }); - } + }, /* Load the console */ - ,loadConsole: function(btn,topic) { + loadConsole: function(btn, topic) { this.console = MODx.load({ - xtype: 'modx-console' - ,register: 'mgr' - ,topic: topic + xtype: 'modx-console', + register: 'mgr', + topic: topic }); this.console.show(btn); - } + }, - ,getConsole: function() { + getConsole: function() { return this.console; } }); -Ext.reg('modx-grid-package-versions',MODx.grid.PackageVersions); +Ext.reg('modx-grid-package-versions', MODx.grid.PackageVersions); /** * @class MODx.window.PurgePackageVersions @@ -114,53 +142,55 @@ Ext.reg('modx-grid-package-versions',MODx.grid.PackageVersions); * @param {Object} config An object of configuration parameters * @xtype modx-window-package-versions-purge */ -MODx.window.PurgePackageVersions = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('package_versions_purge') - ,url: MODx.config.connector_url - ,baseParams: { +MODx.window.PurgePackageVersions = function(config = {}) { + Ext.applyIf(config, { + title: _('package_versions_purge'), + url: MODx.config.connector_url, + baseParams: { action: 'Workspace/Packages/Purge' - } - ,cls: 'modx-confirm' - ,defaults: { border: false } - ,fields: [{ - xtype: 'hidden' - ,name: 'packagename' - ,id: 'modx-ppack-package_name' - ,value: config.packagename - },{ + }, + cls: 'modx-confirm', + defaults: { border: false }, + fields: [{ + xtype: 'hidden', + name: 'packagename', + id: 'modx-ppack-package_name', + value: config.packagename + }, { html: _('package_versions_purge_confirm') - }] - ,saveBtnText: _('package_versions_purge') + }], + saveBtnText: _('package_versions_purge') }); - MODx.window.PurgePackageVersions.superclass.constructor.call(this,config); + MODx.window.PurgePackageVersions.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.PurgePackageVersions,MODx.Window,{ +Ext.extend(MODx.window.PurgePackageVersions, MODx.Window, { submit: function() { - var r = this.config.record; + const { record } = this.config; if (this.fp.getForm().isValid()) { - Ext.getCmp('modx-grid-package-versions').loadConsole(Ext.getBody(),r.topic); + Ext.getCmp('modx-grid-package-versions').loadConsole(Ext.getBody(), record.topic); this.fp.getForm().baseParams = { - action: 'Workspace/Packages/Purge' - ,register: 'mgr' - ,topic: r.topic + action: 'Workspace/Packages/Purge', + register: 'mgr', + topic: record.topic }; this.fp.getForm().submit({ - waitMsg: _('saving') - ,scope: this - ,failure: function(frm,a) { - this.fireEvent('failure',frm,a); - var g = Ext.getCmp('modx-grid-package-versions'); + waitMsg: _('saving'), + scope: this, + failure: function(frm, a) { + this.fireEvent('failure', frm, a); + const g = Ext.getCmp('modx-grid-package-versions'); g.getConsole().fireEvent('complete'); g.refresh(); Ext.Msg.hide(); this.hide(); - } - ,success: function(frm,a) { - this.fireEvent('success',{f:frm,a:a}); - var g = Ext.getCmp('modx-grid-package-versions'); + }, + success: function(frm, a) { + this.fireEvent('success', { + f: frm, + a: a + }); + const g = Ext.getCmp('modx-grid-package-versions'); g.getConsole().fireEvent('complete'); g.refresh(); Ext.Msg.hide(); @@ -170,4 +200,4 @@ Ext.extend(MODx.window.PurgePackageVersions,MODx.Window,{ } } }); -Ext.reg('modx-window-package-versions-purge',MODx.window.PurgePackageVersions); +Ext.reg('modx-window-package-versions-purge', MODx.window.PurgePackageVersions); From 0b0e19fb04d5c0ff18745f0c2136dd6da7ca02f0 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 4 Nov 2024 00:27:20 -0500 Subject: [PATCH 11/39] Package Versions additions Hides actions icon for first (currently installed) package --- .../Workspace/Packages/Version/GetList.php | 26 +++++++++++++------ .../package/package.versions.grid.js | 5 ++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php b/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php index 3f2609b2c3..ed582f28b5 100644 --- a/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php +++ b/core/src/Revolution/Processors/Workspace/Packages/Version/GetList.php @@ -29,6 +29,8 @@ class GetList extends GetListProcessor public $permission = 'packages'; public $languageTopics = ['workspace']; + public $canRemove = false; + /** * @return bool */ @@ -42,6 +44,8 @@ public function initialize() 'dateFormat' => $this->modx->getOption('manager_date_format') . ', ' . $this->modx->getOption('manager_time_format'), 'signature' => false, ]); + $this->canRemove = $this->modx->hasPermission('packages'); + return parent::initialize(); } @@ -74,9 +78,10 @@ public function getData() */ public function prepareRow(xPDOObject $object) { - if ($object->get('installed') === '0000-00-00 00:00:00') { - $object->set('installed', null); - } + $permissions = [ + 'delete' => $this->canRemove + ]; + $installed = !in_array($object->get('installed'), [null, '0000-00-00 00:00:00']); $packageArray = $object->toArray(); $packageArray = $this->parseVersion($object, $packageArray); @@ -85,9 +90,14 @@ public function prepareRow(xPDOObject $object) $packageArray = $this->prepareMenu($object, $packageArray); /* setup description, using either metadata or readme */ - if ($object->get('installed') === null) { + if (!$installed) { $this->currentIndex--; } + if ($this->currentIndex === 0) { + $permissions['delete'] = false; + } + $packageArray['permissions'] = $permissions; + return $packageArray; } @@ -173,11 +183,11 @@ public function getMetaData(modTransportPackage $package, array $packageArray) */ public function prepareMenu(modTransportPackage $package, array $packageArray) { - $notInstalled = $package->get('installed') === null || $package->get('installed') === '0000-00-00 00:00:00'; - $packageArray['iconaction'] = $notInstalled ? 'icon-install' : 'icon-uninstall'; - $packageArray['textaction'] = $notInstalled ? $this->modx->lexicon('install') : $this->modx->lexicon('uninstall'); + $installed = !in_array($package->get('installed'), [null, '0000-00-00 00:00:00']); + $packageArray['iconaction'] = !$installed ? 'icon-install' : 'icon-uninstall'; + $packageArray['textaction'] = !$installed ? $this->modx->lexicon('install') : $this->modx->lexicon('uninstall'); - if ($this->currentIndex > 0 || !$package->get('installed')) { + if ($this->currentIndex > 0 || !$installed) { $packageArray['menu'] = []; $packageArray['menu'][] = [ 'text' => $this->modx->lexicon('package_version_remove'), diff --git a/manager/assets/modext/workspace/package/package.versions.grid.js b/manager/assets/modext/workspace/package/package.versions.grid.js index b7a6668d23..7522130997 100644 --- a/manager/assets/modext/workspace/package/package.versions.grid.js +++ b/manager/assets/modext/workspace/package/package.versions.grid.js @@ -60,6 +60,11 @@ MODx.grid.PackageVersions = function(config = {}) { }] }); MODx.grid.PackageVersions.superclass.constructor.call(this, config); + + this.gridMenuActions = ['delete']; + + this.setUserCanDelete(['packages']); + this.setShowActionsMenu(); }; Ext.extend(MODx.grid.PackageVersions, MODx.grid.Grid, { From 3627ae112f10d0f8733422e9c310cf1114ffaf93 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 4 Nov 2024 10:01:25 -0500 Subject: [PATCH 12/39] Lexicons formatting updates --- .../modext/workspace/lexicon/lexicon.grid.js | 339 +++++++++--------- 1 file changed, 172 insertions(+), 167 deletions(-) diff --git a/manager/assets/modext/workspace/lexicon/lexicon.grid.js b/manager/assets/modext/workspace/lexicon/lexicon.grid.js index 7d7d5440d1..817d14c690 100644 --- a/manager/assets/modext/workspace/lexicon/lexicon.grid.js +++ b/manager/assets/modext/workspace/lexicon/lexicon.grid.js @@ -11,10 +11,10 @@ MODx.grid.Lexicon = function(config = {}) { this.topicFilterValue = MODx.util.url.getParamValue('topic') || 'default'; this.namespaceFilterValue = MODx.util.url.getParamValue('ns') || 'core'; - Ext.applyIf(config,{ - id: 'modx-grid-lexicon' - ,url: MODx.config.connector_url - ,fields: [ + Ext.applyIf(config, { + id: 'modx-grid-lexicon', + url: MODx.config.connector_url, + fields: [ 'name', 'value', 'namespace', @@ -22,48 +22,50 @@ MODx.grid.Lexicon = function(config = {}) { 'language', 'editedon', 'overridden' - ] - ,baseParams: { + ], + baseParams: { action: 'Workspace/Lexicon/GetList', namespace: this.namespaceFilterValue, topic: this.topicFilterValue, language: this.languageFilterValue - } - ,paging: true - ,autosave: true - ,save_action: 'Workspace/Lexicon/UpdateFromGrid' - ,columns: [{ - header: _('name') - ,dataIndex: 'name' - ,width: 200 - ,sortable: true - ,renderer: this._renderStatus - },{ - header: _('value') - ,dataIndex: 'value' - ,width: 500 - ,sortable: false - ,editor: {xtype: 'textarea'} - ,renderer: this._renderStatus - },{ - header: _('last_modified') - ,dataIndex: 'editedon' - ,width: 125 - ,renderer: this._renderLastModDate - }] - ,tbar: { + }, + paging: true, + autosave: true, + save_action: 'Workspace/Lexicon/UpdateFromGrid', + columns: [{ + header: _('name'), + dataIndex: 'name', + width: 200, + sortable: true, + renderer: this._renderStatus + }, { + header: _('value'), + dataIndex: 'value', + width: 500, + sortable: false, + editor: { + xtype: 'textarea' + }, + renderer: this._renderStatus + }, { + header: _('last_modified'), + dataIndex: 'editedon', + width: 125, + renderer: this._renderLastModDate + }], + tbar: { cls: 'has-nested-filters', items: [ { - xtype: 'button' - ,text: _('create') - ,cls: 'primary-button' - ,handler: this.createEntry - ,scope: this - },{ - text: _('lexicon_revert') - ,handler: this.reloadFromBase - ,scope: this + xtype: 'button', + text: _('create'), + cls: 'primary-button', + handler: this.createEntry, + scope: this + }, { + text: _('lexicon_revert'), + handler: this.reloadFromBase, + scope: this }, '->', { @@ -79,8 +81,7 @@ MODx.grid.Lexicon = function(config = {}) { { xtype: 'label', html: _('namespace') - }, - { + }, { xtype: 'modx-combo-namespace', itemId: 'filter-namespace', hideLabel: true, @@ -128,8 +129,7 @@ MODx.grid.Lexicon = function(config = {}) { { xtype: 'label', html: _('language') - }, - { + }, { xtype: 'modx-combo-language', itemId: 'filter-language', hideLabel: true, @@ -178,8 +178,7 @@ MODx.grid.Lexicon = function(config = {}) { { xtype: 'label', html: _('topic') - }, - { + }, { xtype: 'modx-combo-lexicon-topic', itemId: 'filter-topic', hideLabel: true, @@ -220,110 +219,116 @@ MODx.grid.Lexicon = function(config = {}) { ] } }); - MODx.grid.Lexicon.superclass.constructor.call(this,config); + MODx.grid.Lexicon.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.Lexicon,MODx.grid.Grid,{ - console: null +Ext.extend(MODx.grid.Lexicon, MODx.grid.Grid, { + console: null, - ,_renderStatus: function(v,md,rec,ri) { - switch (rec.data.overridden) { + _renderStatus: function(value, metaData, record, rowIndex) { + switch (record.data.overridden) { case 1: - return ''+v+'';break; + return `${value}`; case 2: - return ''+v+''; + return `${value}`; default: - return ''+v+''; + return `${value}`; } - } + }, - ,_renderLastModDate: function(value) { + _renderLastModDate: function(value) { if (Ext.isEmpty(value)) { return '—'; } + return new Date(value * 1000).format(`${MODx.config.manager_date_format} ${MODx.config.manager_time_format}`); + }, - return new Date(value*1000).format(MODx.config.manager_date_format + ' ' + MODx.config.manager_time_format); - } - - ,loadWindow2: function(btn,e,o) { + loadWindow2: function(btn, e, o) { this.menu.record = { namespace: this.getFilterComponent('filter-namespace').getValue(), language: this.getFilterComponent('filter-language').getValue() }; - if (o.xtype != 'modx-window-lexicon-import') { + if (o.xtype !== 'modx-window-lexicon-import') { this.menu.record.topic = this.getFilterComponent('filter-topic').getValue(); } this.loadWindow(btn, e, o); - } + }, - ,reloadFromBase: function() { - namespace = this.getFilterComponent('filter-namespace').getValue(), - topic = this.getFilterComponent('filter-topic').getValue(), - language = this.getFilterComponent('filter-language').getValue(), - registryTopic = '/workspace/lexicon/reload/'; + reloadFromBase: function() { + const + namespace = this.getFilterComponent('filter-namespace').getValue(), + topic = this.getFilterComponent('filter-topic').getValue(), + language = this.getFilterComponent('filter-language').getValue(), + registryTopic = '/workspace/lexicon/reload/' + ; MODx.msg.confirm({ text: _('lexicon_revert_confirm', { - namespace: namespace - ,topic: topic - ,language: language - }) - ,url: this.config.url - ,params: { - action: 'Workspace/Lexicon/ReloadFromBase' - ,register: 'mgr' - ,topic: registryTopic - ,namespace: namespace - ,lexiconTopic: topic - ,language: language - } - ,listeners: { - 'success': { - fn:function() { + namespace: namespace, + topic: topic, + language: language + }), + url: this.config.url, + params: { + action: 'Workspace/Lexicon/ReloadFromBase', + register: 'mgr', + topic: registryTopic, + namespace: namespace, + lexiconTopic: topic, + language: language + }, + listeners: { + success: { + fn: function() { this.console = MODx.load({ - xtype: 'modx-console' - ,register: 'mgr' - ,topic: registryTopic + xtype: 'modx-console', + register: 'mgr', + topic: registryTopic }); - this.console.on('complete',function(){ + this.console.on('complete', function() { this.refresh(); - },this); + }, this); this.console.show(Ext.getBody()); - } - ,scope:this + }, + scope: this } } }); - } + }, - ,revertEntry: function() { - var p = this.menu.record; - p.action = 'Workspace/Lexicon/Revert'; + revertEntry: function() { + const { record } = this.menu; + record.action = 'Workspace/Lexicon/Revert'; MODx.Ajax.request({ - url: this.config.url - ,params: p - ,listeners: { - 'success': {fn:function(r) { - this.refresh(); - },scope:this} + url: this.config.url, + params: record, + listeners: { + success: { + fn: function(r) { + this.refresh(); + }, + scope: this + } } }); - } + }, - ,getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var m = []; - if (r.data.overridden) { - m.push({ - text: _('entry_revert') - ,handler: this.revertEntry + getMenu: function() { + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (record.data.overridden) { + menu.push({ + text: _('entry_revert'), + handler: this.revertEntry }); } - return m; - } + return menu; + }, - ,createEntry: function(btn, e) { + createEntry: function(btn, e) { const record = this.menu.record || {}; record.namespace = this.getFilterComponent('filter-namespace').getValue(); @@ -349,64 +354,64 @@ Ext.extend(MODx.grid.Lexicon,MODx.grid.Grid,{ this.createEntryWindow.show(e.target); } }); -Ext.reg('modx-grid-lexicon',MODx.grid.Lexicon); +Ext.reg('modx-grid-lexicon', MODx.grid.Lexicon); -MODx.window.LexiconEntryCreate = function(config) { - config = config || {}; - this.ident = config.ident || 'lexentc'+Ext.id(); - var r = config.record; - Ext.applyIf(config,{ - title: _('create') - ,url: MODx.config.connector_url - ,action: 'Workspace/Lexicon/Create' - ,fileUpload: true - ,fields: [{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,id: 'modx-'+this.ident+'-name' - ,itemId: 'name' - ,name: 'name' - ,anchor: '100%' - ,msgTarget: 'under' - ,allowBlank: false - },{ - xtype: 'modx-combo-namespace' - ,fieldLabel: _('namespace') - ,name: 'namespace' - ,id: 'modx-'+this.ident+'-namespace' - ,itemId: 'namespace' - ,anchor: '100%' - ,msgTarget: 'under' - ,allowBlank: false - },{ - xtype: 'modx-combo-lexicon-topic' - ,fieldLabel: _('topic') - ,name: 'topic' - ,id: 'modx-'+this.ident+'-topic' - ,itemId: 'topic' - ,anchor: '100%' - ,msgTarget: 'under' - ,allowBlank: false - },{ - xtype: 'modx-combo-language' - ,fieldLabel: _('language') - ,name: 'language' - ,id: 'modx-'+this.ident+'-language' - ,itemId: 'language' - ,anchor: '100%' - ,msgTarget: 'under' - ,allowBlank: false - },{ - xtype: 'textarea' - ,fieldLabel: _('value') - ,id: 'modx-'+this.ident+'-value' - ,itemId: 'value' - ,name: 'value' - ,anchor: '100%' - ,msgTarget: 'under' +MODx.window.LexiconEntryCreate = function(config = {}) { + this.ident = config.ident || `lexentc${Ext.id()}`; + // eslint-disable-next-line no-unused-vars + const r = config.record; + Ext.applyIf(config, { + title: _('create'), + url: MODx.config.connector_url, + action: 'Workspace/Lexicon/Create', + fileUpload: true, + fields: [{ + xtype: 'textfield', + fieldLabel: _('name'), + id: `modx-${this.ident}-name`, + itemId: 'name', + name: 'name', + anchor: '100%', + msgTarget: 'under', + allowBlank: false + }, { + xtype: 'modx-combo-namespace', + fieldLabel: _('namespace'), + name: 'namespace', + id: `modx-${this.ident}-namespace`, + itemId: 'namespace', + anchor: '100%', + msgTarget: 'under', + allowBlank: false + }, { + xtype: 'modx-combo-lexicon-topic', + fieldLabel: _('topic'), + name: 'topic', + id: `modx-${this.ident}-topic`, + itemId: 'topic', + anchor: '100%', + msgTarget: 'under', + allowBlank: false + }, { + xtype: 'modx-combo-language', + fieldLabel: _('language'), + name: 'language', + id: `modx-${this.ident}-language`, + itemId: 'language', + anchor: '100%', + msgTarget: 'under', + allowBlank: false + }, { + xtype: 'textarea', + fieldLabel: _('value'), + id: `modx-${this.ident}-value`, + itemId: 'value', + name: 'value', + anchor: '100%', + msgTarget: 'under' }] }); - MODx.window.LexiconEntryCreate.superclass.constructor.call(this,config); + MODx.window.LexiconEntryCreate.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.LexiconEntryCreate,MODx.Window); -Ext.reg('modx-window-lexicon-entry-create',MODx.window.LexiconEntryCreate); +Ext.extend(MODx.window.LexiconEntryCreate, MODx.Window); +Ext.reg('modx-window-lexicon-entry-create', MODx.window.LexiconEntryCreate); From 10eddfa535f3150695755471d9a4827b98dde65a Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 4 Nov 2024 10:45:53 -0500 Subject: [PATCH 13/39] Lexicons updates Hides actions icon for unchanged Lexicons --- .../Processors/Workspace/Lexicon/GetList.php | 8 +++- .../modext/workspace/lexicon/lexicon.grid.js | 42 ++++++++----------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/core/src/Revolution/Processors/Workspace/Lexicon/GetList.php b/core/src/Revolution/Processors/Workspace/Lexicon/GetList.php index f2e874e510..585ca635f9 100644 --- a/core/src/Revolution/Processors/Workspace/Lexicon/GetList.php +++ b/core/src/Revolution/Processors/Workspace/Lexicon/GetList.php @@ -134,7 +134,7 @@ function parseArray($needle, array $haystack = []) ksort($entries); $entries = array_slice($entries, $this->getProperty('start'), $this->getProperty('limit'), true); - /* loop through */ + // Note that for Lexicons, the 'edit' permission correlates with the ability to revert a customized entry $list = []; foreach ($entries as $name => $value) { $entryArray = [ @@ -146,6 +146,9 @@ function parseArray($needle, array $haystack = []) 'createdon' => null, 'editedon' => null, 'overridden' => 0, + 'permissions' => [ + 'edit' => false + ] ]; /* if override in db, load */ if (array_key_exists($name, $dbEntries)) { @@ -155,6 +158,9 @@ function parseArray($needle, array $haystack = []) $entryArray['editedon'] = strtotime($entryArray['editedon']) ?: strtotime($entryArray['createdon']); $entryArray['overridden'] = 1; + $entryArray['permissions'] = [ + 'edit' => true + ]; } $list[] = $entryArray; } diff --git a/manager/assets/modext/workspace/lexicon/lexicon.grid.js b/manager/assets/modext/workspace/lexicon/lexicon.grid.js index 817d14c690..5fa0ea5e5f 100644 --- a/manager/assets/modext/workspace/lexicon/lexicon.grid.js +++ b/manager/assets/modext/workspace/lexicon/lexicon.grid.js @@ -31,6 +31,7 @@ MODx.grid.Lexicon = function(config = {}) { }, paging: true, autosave: true, + preventSaveRefresh: false, save_action: 'Workspace/Lexicon/UpdateFromGrid', columns: [{ header: _('name'), @@ -220,6 +221,12 @@ MODx.grid.Lexicon = function(config = {}) { } }); MODx.grid.Lexicon.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit']; + + // Note there are currently no action-specific permissions for Lexicons + this.setUserCanEdit(['lexicons']); + this.setShowActionsMenu(); }; Ext.extend(MODx.grid.Lexicon, MODx.grid.Grid, { console: null, @@ -358,57 +365,42 @@ Ext.reg('modx-grid-lexicon', MODx.grid.Lexicon); MODx.window.LexiconEntryCreate = function(config = {}) { this.ident = config.ident || `lexentc${Ext.id()}`; - // eslint-disable-next-line no-unused-vars - const r = config.record; Ext.applyIf(config, { title: _('create'), url: MODx.config.connector_url, action: 'Workspace/Lexicon/Create', fileUpload: true, + formDefaults: { + anchor: '100%', + msgTarget: 'under', + allowBlank: false + }, fields: [{ xtype: 'textfield', fieldLabel: _('name'), - id: `modx-${this.ident}-name`, itemId: 'name', - name: 'name', - anchor: '100%', - msgTarget: 'under', - allowBlank: false + name: 'name' }, { xtype: 'modx-combo-namespace', fieldLabel: _('namespace'), name: 'namespace', - id: `modx-${this.ident}-namespace`, - itemId: 'namespace', - anchor: '100%', - msgTarget: 'under', - allowBlank: false + itemId: 'namespace' }, { xtype: 'modx-combo-lexicon-topic', fieldLabel: _('topic'), name: 'topic', - id: `modx-${this.ident}-topic`, - itemId: 'topic', - anchor: '100%', - msgTarget: 'under', - allowBlank: false + itemId: 'topic' }, { xtype: 'modx-combo-language', fieldLabel: _('language'), name: 'language', - id: `modx-${this.ident}-language`, - itemId: 'language', - anchor: '100%', - msgTarget: 'under', - allowBlank: false + itemId: 'language' }, { xtype: 'textarea', fieldLabel: _('value'), - id: `modx-${this.ident}-value`, itemId: 'value', name: 'value', - anchor: '100%', - msgTarget: 'under' + allowBlank: true }] }); MODx.window.LexiconEntryCreate.superclass.constructor.call(this, config); From 6038d3dc315c6a1acdcbc3f10f6772a5893dfa0e Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 4 Nov 2024 22:42:32 -0500 Subject: [PATCH 14/39] FC Profiles and Sets Formatting, style updates only --- .../Processors/Security/Forms/Set/GetList.php | 6 +- .../modext/widgets/fc/modx.grid.fcprofile.js | 532 +++++++------ .../modext/widgets/fc/modx.grid.fcset.js | 738 +++++++++--------- 3 files changed, 667 insertions(+), 609 deletions(-) diff --git a/core/src/Revolution/Processors/Security/Forms/Set/GetList.php b/core/src/Revolution/Processors/Security/Forms/Set/GetList.php index 1fcd77a4cf..dbfb334393 100644 --- a/core/src/Revolution/Processors/Security/Forms/Set/GetList.php +++ b/core/src/Revolution/Processors/Security/Forms/Set/GetList.php @@ -1,4 +1,5 @@ setDefaultProperties(['profile' => 0, 'query' => '']); + $this->setDefaultProperties([ + 'profile' => 0, + 'query' => '' + ]); $this->canEdit = $this->modx->hasPermission('save'); $this->canRemove = $this->modx->hasPermission('remove'); return parent::initialize(); diff --git a/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js b/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js index d397e471b2..c2fd2e7e84 100644 --- a/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js +++ b/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js @@ -4,37 +4,39 @@ * @param {Object} config An object of configuration options * @xtype modx-panel-fc-profiles */ -MODx.panel.FCProfiles = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-fc-profiles' - ,cls: 'container' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('form_customization') - ,id: 'modx-fcp-header' - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('profiles') - ,autoHeight: true - ,layout: "form" - ,items: [{ - html: '

          '+_('form_customization_msg')+'

          ' - ,xtype: 'modx-description' - },{ - title: '' - ,preventRender: true - ,xtype: 'modx-grid-fc-profile' - ,cls:'main-wrapper' +MODx.panel.FCProfiles = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-fc-profiles', + cls: 'container', + defaults: { + collapsible: false, + autoHeight: true + }, + items: [{ + html: _('form_customization'), + id: 'modx-fcp-header', + xtype: 'modx-header' + }, MODx.getPageStructure([{ + title: _('profiles'), + autoHeight: true, + layout: 'form', + items: [{ + html: `

          ${_('form_customization_msg')}

          `, + xtype: 'modx-description' + }, { + title: '', + preventRender: true, + xtype: 'modx-grid-fc-profile', + cls: 'main-wrapper' }] - }],{ + }], { id: 'modx-form-customization-tabs' })] }); - MODx.panel.FCProfiles.superclass.constructor.call(this,config); + MODx.panel.FCProfiles.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.FCProfiles,MODx.FormPanel); -Ext.reg('modx-panel-fc-profiles',MODx.panel.FCProfiles); +Ext.extend(MODx.panel.FCProfiles, MODx.FormPanel); +Ext.reg('modx-panel-fc-profiles', MODx.panel.FCProfiles); /** * @class MODx.grid.FCProfile @@ -42,16 +44,15 @@ Ext.reg('modx-panel-fc-profiles',MODx.panel.FCProfiles); * @param {Object} config An object of configuration properties * @xtype modx-grid-fc-profile */ -MODx.grid.FCProfile = function(config) { - config = config || {}; +MODx.grid.FCProfile = function(config = {}) { this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - id: 'modx-grid-fc-profile' - ,url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + id: 'modx-grid-fc-profile', + url: MODx.config.connector_url, + baseParams: { action: 'Security/Forms/Profile/GetList' - } - ,fields: [ + }, + fields: [ 'id', 'name', 'description', @@ -60,69 +61,72 @@ MODx.grid.FCProfile = function(config) { 'rank', 'sets', 'perm' - ] - ,paging: true - ,autosave: true - ,save_action: 'Security/Forms/Profile/UpdateFromGrid' - ,sm: this.sm - ,remoteSort: true - ,columns: [this.sm,{ - header: _('id') - ,dataIndex: 'id' - ,width: 40 - ,sortable: true - },{ - header: _('name') - ,dataIndex: 'name' - ,width: 200 - ,sortable: true - ,editor: { xtype: 'textfield' } - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/forms/profile/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 250 - ,sortable: true - ,editor: { xtype: 'textarea' } - },{ - header: _('usergroups') - ,dataIndex: 'usergroups' - ,width: 150 - }] - ,viewConfig: { - forceFit:true - ,enableRowBody:true - ,scrollOffset: 0 - ,autoFill: true - ,showPreview: true - ,getRowClass : function(rec, ri, p){ + ], + paging: true, + autosave: true, + save_action: 'Security/Forms/Profile/UpdateFromGrid', + sm: this.sm, + remoteSort: true, + columns: [this.sm, { + header: _('id'), + dataIndex: 'id', + width: 40, + sortable: true + }, { + header: _('name'), + dataIndex: 'name', + width: 200, + sortable: true, + editor: { xtype: 'textfield' }, + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=security/forms/profile/update&id=${record.data.id}` + }); + }, + scope: this + } + }, { + header: _('description'), + dataIndex: 'description', + width: 250, + sortable: true, + editor: { xtype: 'textarea' } + }, { + header: _('usergroups'), + dataIndex: 'usergroups', + width: 150 + }], + viewConfig: { + forceFit: true, + enableRowBody: true, + scrollOffset: 0, + autoFill: true, + showPreview: true, + getRowClass: function(rec, ri, p) { return rec.data.active ? 'grid-row-active' : 'grid-row-inactive'; } - } - ,tbar: [ + }, + tbar: [ { - text: _('create') - ,scope: this - ,handler: this.createProfile - ,cls:'primary-button' - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_activate') - ,handler: this.activateSelected - ,scope: this - },{ - text: _('selected_deactivate') - ,handler: this.deactivateSelected - ,scope: this - },{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + text: _('create'), + scope: this, + handler: this.createProfile, + cls: 'primary-button' + }, { + text: _('bulk_actions'), + menu: [{ + text: _('selected_activate'), + handler: this.activateSelected, + scope: this + }, { + text: _('selected_deactivate'), + handler: this.deactivateSelected, + scope: this + }, { + text: _('selected_remove'), + handler: this.removeSelected, + scope: this }] }, '->', @@ -130,185 +134,210 @@ MODx.grid.FCProfile = function(config) { this.getClearFiltersButton() ] }); - MODx.grid.FCProfile.superclass.constructor.call(this,config); - this.on('render',function() { this.getStore().reload(); },this); + MODx.grid.FCProfile.superclass.constructor.call(this, config); + this.on('render', function() { this.getStore().reload(); }, this); }; -Ext.extend(MODx.grid.FCProfile,MODx.grid.Grid,{ +Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.perm; - - var m = []; + const + record = this.getSelectionModel().getSelected(), + menu = [], + p = record.data.perm + ; if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_activate') - ,handler: this.activateSelected + menu.push({ + text: _('selected_activate'), + handler: this.activateSelected }); - m.push({ - text: _('selected_deactivate') - ,handler: this.deactivateSelected + menu.push({ + text: _('selected_deactivate'), + handler: this.deactivateSelected }); - m.push('-'); - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected + menu.push('-'); + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected }); } else { - if (p.indexOf('pedit') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateProfile - },{ - text: _('duplicate') - ,handler: this.duplicateProfile - },'-'); - if (r.data.active) { - m.push({ - text: _('deactivate') - ,handler: this.deactivateProfile + if (p.indexOf('pedit') !== -1) { + menu.push({ + text: _('edit'), + handler: this.updateProfile + }, { + text: _('duplicate'), + handler: this.duplicateProfile + }, '-'); + if (record.data.active) { + menu.push({ + text: _('deactivate'), + handler: this.deactivateProfile }); } else { - m.push({ - text: _('activate') - ,handler: this.activateProfile + menu.push({ + text: _('activate'), + handler: this.activateProfile }); } } - if (p.indexOf('premove') != -1) { - m.push('-',{ - text: _('delete') - ,handler: this.confirm.createDelegate(this,['Security/Forms/Profile/Remove','profile_remove_confirm']) + if (p.indexOf('premove') !== -1) { + menu.push('-', { + text: _('delete'), + handler: this.confirm.createDelegate(this, ['Security/Forms/Profile/Remove', 'profile_remove_confirm']) }); } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (menu.length > 0) { + this.addContextMenuItem(menu); } - } + }, - ,createProfile: function(btn,e) { + createProfile: function(btn, e) { if (!this.windows.cpro) { this.windows.cpro = MODx.load({ - xtype: 'modx-window-fc-profile-create' - ,listeners: { - 'success': {fn:function(r) { - this.refresh(); - },scope:this} + xtype: 'modx-window-fc-profile-create', + listeners: { + success: { + fn: function(r) { + this.refresh(); + }, + scope: this + } } }); } this.windows.cpro.reset(); this.windows.cpro.show(e.target); - } + }, - ,updateProfile: function(btn,e) { - var r = this.menu.record; - location.href = '?a=security/forms/profile/update&id='+r.id; - } + updateProfile: function(btn, e) { + const { record } = this.menu; + window.location.href = `?a=security/forms/profile/update&id=${record.id}`; + }, - ,duplicateProfile: function(btn,e) { + duplicateProfile: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'security/forms/profile/duplicate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'security/forms/profile/duplicate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } + }, - ,activateProfile: function(btn,e) { + activateProfile: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Profile/Activate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Profile/Activate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } + }, - ,deactivateProfile: function(btn,e) { + deactivateProfile: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Profile/Deactivate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Profile/Deactivate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } - - ,activateSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + activateSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Profile/ActivateMultiple' - ,profiles: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Profile/ActivateMultiple', + profiles: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,deactivateSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + deactivateSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Profile/DeactivateMultiple' - ,profiles: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Profile/DeactivateMultiple', + profiles: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + removeSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('profile_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Security/Forms/Profile/RemoveMultiple' - ,profiles: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + title: _('selected_remove'), + text: _('profile_remove_multiple_confirm'), + url: this.config.url, + params: { + action: 'Security/Forms/Profile/RemoveMultiple', + profiles: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; } }); -Ext.reg('modx-grid-fc-profile',MODx.grid.FCProfile); +Ext.reg('modx-grid-fc-profile', MODx.grid.FCProfile); /** * @class MODx.window.CreateFCProfile @@ -316,39 +345,38 @@ Ext.reg('modx-grid-fc-profile',MODx.grid.FCProfile); * @param {Object} config An object of options. * @xtype modx-window-fc-profile-create */ -MODx.window.CreateFCProfile = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('create') - ,url: MODx.config.connector_url - ,action: 'Security/Forms/Profile/Create' - ,fields: [{ - xtype: 'textfield' - ,name: 'name' - ,fieldLabel: _('name') - ,id: 'modx-fccp-name' - ,allowBlank: false - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,id: 'modx-fccp-description' - ,anchor: '100%' - },{ - xtype: 'xcheckbox' - ,boxLabel: _('active') - ,hideLabel: true - ,name: 'active' - ,id: 'modx-fccp-active' - ,inputValue: 1 - ,value: 1 - ,checked: true - ,anchor: '100%' - }] - ,keys: [] +MODx.window.CreateFCProfile = function(config = {}) { + Ext.applyIf(config, { + title: _('create'), + url: MODx.config.connector_url, + action: 'Security/Forms/Profile/Create', + fields: [{ + xtype: 'textfield', + name: 'name', + fieldLabel: _('name'), + id: 'modx-fccp-name', + allowBlank: false, + anchor: '100%' + }, { + xtype: 'textarea', + name: 'description', + fieldLabel: _('description'), + id: 'modx-fccp-description', + anchor: '100%' + }, { + xtype: 'xcheckbox', + boxLabel: _('active'), + hideLabel: true, + name: 'active', + id: 'modx-fccp-active', + inputValue: 1, + value: 1, + checked: true, + anchor: '100%' + }], + keys: [] }); - MODx.window.CreateFCProfile.superclass.constructor.call(this,config); + MODx.window.CreateFCProfile.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateFCProfile,MODx.Window); -Ext.reg('modx-window-fc-profile-create',MODx.window.CreateFCProfile); +Ext.extend(MODx.window.CreateFCProfile, MODx.Window); +Ext.reg('modx-window-fc-profile-create', MODx.window.CreateFCProfile); diff --git a/manager/assets/modext/widgets/fc/modx.grid.fcset.js b/manager/assets/modext/widgets/fc/modx.grid.fcset.js index c4f95b1c74..988d05b005 100644 --- a/manager/assets/modext/widgets/fc/modx.grid.fcset.js +++ b/manager/assets/modext/widgets/fc/modx.grid.fcset.js @@ -1,12 +1,12 @@ MODx.grid.FCSet = function(config = {}) { this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - id: 'modx-grid-fc-set' - ,url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + id: 'modx-grid-fc-set', + url: MODx.config.connector_url, + baseParams: { action: 'Security/Forms/Set/GetList' - } - ,fields: [ + }, + fields: [ 'id', 'profile', 'action', @@ -20,337 +20,364 @@ MODx.grid.FCSet = function(config = {}) { 'constraint_class', 'rules', 'perm' - ] - ,paging: true - ,autosave: true - ,preventSaveRefresh: false - ,save_action: 'Security/Forms/Set/UpdateFromGrid' - ,sm: this.sm - ,remoteSort: true - ,autoExpandColumn: 'controller' - ,columns: [this.sm,{ - header: _('id') - ,dataIndex: 'id' - ,width: 40 - ,sortable: true - },{ - header: _('action') - ,dataIndex: 'action' - ,width: 200 - ,editable: true - ,sortable: true - ,editor: { + ], + paging: true, + autosave: true, + preventSaveRefresh: false, + save_action: 'Security/Forms/Set/UpdateFromGrid', + sm: this.sm, + remoteSort: true, + autoExpandColumn: 'controller', + columns: [this.sm, { + header: _('id'), + dataIndex: 'id', + width: 40, + sortable: true + }, { + header: _('action'), + dataIndex: 'action', + width: 200, + editable: true, + sortable: true, + editor: { xtype: 'modx-combo-fc-action', renderer: true } - },{ - header: _('description') - ,dataIndex: 'description' - ,width: 200 - ,editable: true - ,sortable: true - ,editor: { + }, { + header: _('description'), + dataIndex: 'description', + width: 200, + editable: true, + sortable: true, + editor: { xtype: 'textarea', renderer: true } - },{ - header: _('template') - ,dataIndex: 'template' - ,width: 150 - ,sortable: true - ,editable: true - ,editor: { + }, { + header: _('template'), + dataIndex: 'template', + width: 150, + sortable: true, + editable: true, + editor: { xtype: 'modx-combo-template', renderer: true } - },{ - header: _('constraint_field') - ,dataIndex: 'constraint_field' - ,width: 200 - ,editable: true - ,sortable: false - ,editor: { + }, { + header: _('constraint_field'), + dataIndex: 'constraint_field', + width: 200, + editable: true, + sortable: false, + editor: { xtype: 'textfield', renderer: true } - },{ - header: _('constraint') - ,dataIndex: 'constraint' - ,width: 200 - ,editable: true - ,sortable: false - ,editor: { + }, { + header: _('constraint'), + dataIndex: 'constraint', + width: 200, + editable: true, + sortable: false, + editor: { xtype: 'textfield', renderer: true } - }] - ,viewConfig: { - forceFit:true - ,enableRowBody:true - ,scrollOffset: 0 - ,autoFill: true - ,showPreview: true - ,getRowClass : function(rec, ri, p){ + }], + viewConfig: { + forceFit: true, + enableRowBody: true, + scrollOffset: 0, + autoFill: true, + showPreview: true, + getRowClass: function(rec, ri, p) { return rec.data.active ? 'grid-row-active' : 'grid-row-inactive'; } - } - ,tbar: [ + }, + tbar: [ { - text: _('create') - ,cls: 'primary-button' - ,scope: this - ,handler: this.createSet - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_activate') - ,handler: this.activateSelected - ,scope: this - },{ - text: _('selected_deactivate') - ,handler: this.deactivateSelected - ,scope: this - },{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + text: _('create'), + cls: 'primary-button', + scope: this, + handler: this.createSet + }, { + text: _('bulk_actions'), + menu: [{ + text: _('selected_activate'), + handler: this.activateSelected, + scope: this + }, { + text: _('selected_deactivate'), + handler: this.deactivateSelected, + scope: this + }, { + text: _('selected_remove'), + handler: this.removeSelected, + scope: this }] - },{ - text: _('import') - ,handler: this.importSet - ,scope: this + }, { + text: _('import'), + handler: this.importSet, + scope: this }, '->', this.getQueryFilterField(), this.getClearFiltersButton() ] }); - MODx.grid.FCSet.superclass.constructor.call(this,config); + MODx.grid.FCSet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.FCSet,MODx.grid.Grid,{ +Ext.extend(MODx.grid.FCSet, MODx.grid.Grid, { getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.perm; - - var m = []; + const + record = this.getSelectionModel().getSelected(), + menu = [], + p = record.data.perm + ; if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_activate') - ,handler: this.activateSelected + menu.push({ + text: _('selected_activate'), + handler: this.activateSelected }); - m.push({ - text: _('selected_deactivate') - ,handler: this.deactivateSelected + menu.push({ + text: _('selected_deactivate'), + handler: this.deactivateSelected }); - m.push('-'); - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected + menu.push('-'); + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected }); } else { - if (p.indexOf('pedit') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateSet + if (p.indexOf('pedit') !== -1) { + menu.push({ + text: _('edit'), + handler: this.updateSet }); - m.push({ - text: _('duplicate') - ,handler: this.duplicateSet + menu.push({ + text: _('duplicate'), + handler: this.duplicateSet }); - m.push({ - text: _('export') - ,handler: this.exportSet + menu.push({ + text: _('export'), + handler: this.exportSet }); - m.push('-'); - if (r.data.active) { - m.push({ - text: _('deactivate') - ,handler: this.deactivateSet + menu.push('-'); + if (record.data.active) { + menu.push({ + text: _('deactivate'), + handler: this.deactivateSet }); } else { - m.push({ - text: _('activate') - ,handler: this.activateSet + menu.push({ + text: _('activate'), + handler: this.activateSet }); } } - if (p.indexOf('premove') != -1) { - m.push('-',{ - text: _('delete') - ,handler: this.confirm.createDelegate(this,['Security/Forms/Set/Remove','set_remove_confirm']) + if (p.indexOf('premove') !== -1) { + menu.push('-', { + text: _('delete'), + handler: this.confirm.createDelegate(this, ['Security/Forms/Set/Remove', 'set_remove_confirm']) }); } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (menu.length > 0) { + this.addContextMenuItem(menu); } - } + }, - ,exportSet: function(btn,e) { - var id = this.menu.record.id; + exportSet: function(btn, e) { + const { id } = this.menu.record; MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Set/Export' - ,id: id - } - ,listeners: { - 'success': {fn:function(r) { - location.href = this.config.url+'?action=Security/Forms/Set/Export&download='+r.message+'&id='+id+'&HTTP_MODAUTH='+MODx.siteId; - },scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Set/Export', + id: id + }, + listeners: { + success: { + fn: function(response) { + window.location.href = `${this.config.url}?action=Security/Forms/Set/Export&download=${response.message}&id=${id}&HTTP_MODAUTH=${MODx.siteId}`; + }, + scope: this + } } }); - } + }, - ,importSet: function(btn,e) { - var r = { + importSet: function(btn, e) { + const record = { profile: MODx.request.id }; if (!this.windows.impset) { this.windows.impset = MODx.load({ - xtype: 'modx-window-fc-set-import' - ,record: r - ,listeners: { - 'success': {fn:function(o) { - this.refresh(); - },scope:this} + xtype: 'modx-window-fc-set-import', + record: record, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); } this.windows.impset.reset(); - this.windows.impset.setValues(r); + this.windows.impset.setValues(record); this.windows.impset.show(e.target); - } + }, - ,createSet: function(btn,e) { - var r = { - profile: MODx.request.id - ,active: true + createSet: function(btn, e) { + const record = { + profile: MODx.request.id, + active: true }; if (!this.windows.cset) { this.windows.cset = MODx.load({ - xtype: 'modx-window-fc-set-create' - ,record: r - ,listeners: { - 'success': {fn:function(r) { - this.refresh(); - },scope:this} + xtype: 'modx-window-fc-set-create', + record: record, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); } this.windows.cset.reset(); - this.windows.cset.setValues(r); + this.windows.cset.setValues(record); this.windows.cset.show(e.target); - } + }, - ,updateSet: function(btn,e) { - var r = this.menu.record; - location.href = '?a=security/forms/set/update&id='+r.id; - } + updateSet: function(btn, e) { + const { record } = this.menu; + window.location.href = `?a=security/forms/set/update&id=${record.id}`; + }, - ,duplicateSet: function(btn,e) { + duplicateSet: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'security/forms/set/duplicate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'security/forms/set/duplicate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } + }, - ,activateSet: function(btn,e) { + activateSet: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Set/Activate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Set/Activate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } + }, - ,deactivateSet: function(btn,e) { + deactivateSet: function(btn, e) { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Set/Deactivate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Set/Deactivate', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } - - ,activateSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + activateSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Set/ActivateMultiple' - ,sets: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Set/ActivateMultiple', + sets: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,deactivateSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + deactivateSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/Forms/Set/DeactivateMultiple' - ,sets: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'Security/Forms/Set/DeactivateMultiple', + sets: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + removeSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('set_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Security/Forms/Set/RemoveMultiple' - ,sets: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + title: _('selected_remove'), + text: _('set_remove_multiple_confirm'), + url: this.config.url, + params: { + action: 'Security/Forms/Set/RemoveMultiple', + sets: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; } }); -Ext.reg('modx-grid-fc-set',MODx.grid.FCSet); +Ext.reg('modx-grid-fc-set', MODx.grid.FCSet); /** * @class MODx.window.CreateFCSet @@ -360,85 +387,85 @@ Ext.reg('modx-grid-fc-set',MODx.grid.FCSet); */ MODx.window.CreateFCSet = function(config = {}) { Ext.applyIf(config, { - title: _('create') - ,url: MODx.config.connector_url - ,action: 'Security/Forms/Set/Create' - ,width: 600 - ,fields: [{ - xtype: 'hidden' - ,name: 'profile' - ,value: MODx.request.id - },{ - xtype: 'hidden' - ,fieldLabel: _('constraint_class') - ,name: 'constraint_class' - ,allowBlank: true - ,value: 'MODX\\Revolution\\modResource' - },{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,border: false - } - ,items: [{ - columnWidth: .5 - ,defaults: { + title: _('create'), + url: MODx.config.connector_url, + action: 'Security/Forms/Set/Create', + width: 600, + fields: [{ + xtype: 'hidden', + name: 'profile', + value: MODx.request.id + }, { + xtype: 'hidden', + fieldLabel: _('constraint_class'), + name: 'constraint_class', + allowBlank: true, + value: 'MODX\\Revolution\\modResource' + }, { + layout: 'column', + border: false, + defaults: { + layout: 'form', + labelAlign: 'top', + border: false + }, + items: [{ + columnWidth: 0.5, + defaults: { anchor: '100%', msgTarget: 'under', validationEvent: 'change', validateOnBlur: false - } - ,items: [{ - fieldLabel: _('action') - ,name: 'action_id' - ,hiddenName: 'action_id' - ,id: 'modx-fcsc-action' - ,xtype: 'modx-combo-fc-action' - ,editable: false - ,allowBlank: false - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,id: 'modx-fcsc-description' - },{ - xtype: 'xcheckbox' - ,boxLabel: _('active') - ,hideLabel: true - ,name: 'active' - ,inputValue: 1 - ,value: 1 - ,checked: true + }, + items: [{ + fieldLabel: _('action'), + name: 'action_id', + hiddenName: 'action_id', + id: 'modx-fcsc-action', + xtype: 'modx-combo-fc-action', + editable: false, + allowBlank: false + }, { + xtype: 'textarea', + name: 'description', + fieldLabel: _('description'), + id: 'modx-fcsc-description' + }, { + xtype: 'xcheckbox', + boxLabel: _('active'), + hideLabel: true, + name: 'active', + inputValue: 1, + value: 1, + checked: true }] - },{ - columnWidth: .5 - ,defaults: { + }, { + columnWidth: 0.5, + defaults: { anchor: '100%', msgTarget: 'under', validationEvent: 'change', validateOnBlur: false - } - ,items: [{ - xtype: 'modx-combo-template' - ,name: 'template' - ,hiddenName: 'template' - ,fieldLabel: _('template') - ,description: MODx.expandHelp ? '' : _('set_template_desc') - ,id: 'modx-fcsc-template' - ,baseParams: { action: 'Element/Template/GetList', combo: true } - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-fcsc-template' - ,html: _('set_template_desc') - ,cls: 'desc-under' - },{ - xtype: 'textfield' - ,fieldLabel: _('constraint_field') - ,description: MODx.expandHelp ? '' : _('set_constraint_field_desc') - ,name: 'constraint_field' - ,listeners: { + }, + items: [{ + xtype: 'modx-combo-template', + name: 'template', + hiddenName: 'template', + fieldLabel: _('template'), + description: MODx.expandHelp ? '' : _('set_template_desc'), + id: 'modx-fcsc-template', + baseParams: { action: 'Element/Template/GetList', combo: true } + }, { + xtype: MODx.expandHelp ? 'label' : 'hidden', + forId: 'modx-fcsc-template', + html: _('set_template_desc'), + cls: 'desc-under' + }, { + xtype: 'textfield', + fieldLabel: _('constraint_field'), + description: MODx.expandHelp ? '' : _('set_constraint_field_desc'), + name: 'constraint_field', + listeners: { change: { fn: function(cmp, newValue, oldValue) { if (!Ext.isEmpty(newValue)) { @@ -451,16 +478,16 @@ MODx.window.CreateFCSet = function(config = {}) { scope: this } } - },{ - xtype: MODx.expandHelp ? 'box' : 'hidden' - ,html: _('set_constraint_field_desc') - ,cls: 'desc-under' - },{ - xtype: 'textfield' - ,fieldLabel: _('constraint') - ,description: MODx.expandHelp ? '' : _('set_constraint_desc') - ,name: 'constraint' - ,listeners: { + }, { + xtype: MODx.expandHelp ? 'box' : 'hidden', + html: _('set_constraint_field_desc'), + cls: 'desc-under' + }, { + xtype: 'textfield', + fieldLabel: _('constraint'), + description: MODx.expandHelp ? '' : _('set_constraint_desc'), + name: 'constraint', + listeners: { change: { fn: function(cmp, newValue, oldValue) { if (!Ext.isEmpty(newValue)) { @@ -473,19 +500,19 @@ MODx.window.CreateFCSet = function(config = {}) { scope: this } } - },{ - xtype: MODx.expandHelp ? 'box' : 'hidden' - ,html: _('set_constraint_desc') - ,cls: 'desc-under' + }, { + xtype: MODx.expandHelp ? 'box' : 'hidden', + html: _('set_constraint_desc'), + cls: 'desc-under' }] }] - }] - ,keys: [] + }], + keys: [] }); - MODx.window.CreateFCSet.superclass.constructor.call(this,config); + MODx.window.CreateFCSet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateFCSet,MODx.Window); -Ext.reg('modx-window-fc-set-create',MODx.window.CreateFCSet); +Ext.extend(MODx.window.CreateFCSet, MODx.Window); +Ext.reg('modx-window-fc-set-create', MODx.window.CreateFCSet); /** * @class MODx.window.ImportFCSet @@ -493,34 +520,33 @@ Ext.reg('modx-window-fc-set-create',MODx.window.CreateFCSet); * @param {Object} config An object of options. * @xtype modx-window-fc-set-import */ -MODx.window.ImportFCSet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('import') - ,id: 'modx-window-fc-set-import' - ,url: MODx.config.connector_url - ,action: 'Security/Forms/Set/Import' - ,fileUpload: true - ,saveBtnText: _('import') - ,fields: [{ - xtype: 'hidden' - ,name: 'profile' - ,value: MODx.request.id - },{ - html: _('set_import_msg') - ,id: 'modx-impset-desc' - ,xtype: 'modx-description' - ,style: 'margin-bottom: 10px;' - },{ - xtype: 'fileuploadfield' - ,fieldLabel: _('file') - ,buttonText: _('upload.buttons.upload') - ,name: 'file' - ,id: 'modx-impset-file' - ,anchor: '100%' +MODx.window.ImportFCSet = function(config = {}) { + Ext.applyIf(config, { + title: _('import'), + id: 'modx-window-fc-set-import', + url: MODx.config.connector_url, + action: 'Security/Forms/Set/Import', + fileUpload: true, + saveBtnText: _('import'), + fields: [{ + xtype: 'hidden', + name: 'profile', + value: MODx.request.id + }, { + html: _('set_import_msg'), + id: 'modx-impset-desc', + xtype: 'modx-description', + style: 'margin-bottom: 10px;' + }, { + xtype: 'fileuploadfield', + fieldLabel: _('file'), + buttonText: _('upload.buttons.upload'), + name: 'file', + id: 'modx-impset-file', + anchor: '100%' }] }); - MODx.window.ImportFCSet.superclass.constructor.call(this,config); + MODx.window.ImportFCSet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.ImportFCSet,MODx.Window); -Ext.reg('modx-window-fc-set-import',MODx.window.ImportFCSet); +Ext.extend(MODx.window.ImportFCSet, MODx.Window); +Ext.reg('modx-window-fc-set-import', MODx.window.ImportFCSet); From de2a6b5f13b2407c4135ac2b78c45a906f84a340 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Tue, 5 Nov 2024 12:23:25 -0500 Subject: [PATCH 15/39] FC Profiles and Sets additions Updates display of and ability to select row actions (gear icon, bulk actions button); includes update to getViewConfig method in base grid class --- .../Security/Forms/Profile/GetList.php | 26 +++-- .../Processors/Security/Forms/Set/GetList.php | 24 ++-- .../assets/modext/widgets/core/modx.grid.js | 29 ++++- .../modext/widgets/fc/modx.grid.fcprofile.js | 108 ++++++------------ .../modext/widgets/fc/modx.grid.fcset.js | 88 ++++---------- 5 files changed, 108 insertions(+), 167 deletions(-) diff --git a/core/src/Revolution/Processors/Security/Forms/Profile/GetList.php b/core/src/Revolution/Processors/Security/Forms/Profile/GetList.php index 907f2106a0..32c354b730 100644 --- a/core/src/Revolution/Processors/Security/Forms/Profile/GetList.php +++ b/core/src/Revolution/Processors/Security/Forms/Profile/GetList.php @@ -28,6 +28,8 @@ class GetList extends GetListProcessor public $classKey = modFormCustomizationProfile::class; public $languageTopics = ['formcustomization']; public $permission = 'customize_forms'; + + public $canCreate = false; public $canEdit = false; public $canRemove = false; @@ -36,8 +38,12 @@ class GetList extends GetListProcessor */ public function initialize() { - $this->setDefaultProperties(['query' => '']); - $this->canEdit = $this->modx->hasPermission('save'); + $this->setDefaultProperties([ + 'query' => '' + ]); + $canSave = $this->modx->hasPermission('save'); + $this->canCreate = $canSave; + $this->canEdit = $canSave; $this->canRemove = $this->modx->hasPermission('remove'); return parent::initialize(); } @@ -74,14 +80,12 @@ public function getData() */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); - $objectArray['perm'] = []; - if ($this->canEdit) { - $objectArray['perm'][] = 'pedit'; - } - if ($this->canRemove) { - $objectArray['perm'][] = 'premove'; - } - return $objectArray; + $profileArray = $object->toArray(); + $profileArray['permissions'] = [ + 'create' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + return $profileArray; } } diff --git a/core/src/Revolution/Processors/Security/Forms/Set/GetList.php b/core/src/Revolution/Processors/Security/Forms/Set/GetList.php index dbfb334393..4ceffddd44 100644 --- a/core/src/Revolution/Processors/Security/Forms/Set/GetList.php +++ b/core/src/Revolution/Processors/Security/Forms/Set/GetList.php @@ -31,6 +31,8 @@ class GetList extends GetListProcessor public $languageTopics = ['formcustomization']; public $permission = 'customize_forms'; public $defaultSortField = 'action'; + + public $canCreate = false; public $canEdit = false; public $canRemove = false; @@ -43,7 +45,9 @@ public function initialize() 'profile' => 0, 'query' => '' ]); - $this->canEdit = $this->modx->hasPermission('save'); + $canSave = $this->modx->hasPermission('save'); + $this->canCreate = $canSave; + $this->canEdit = $canSave; $this->canRemove = $this->modx->hasPermission('remove'); return parent::initialize(); } @@ -87,7 +91,7 @@ public function prepareQueryAfterCount(xPDOQuery $c) */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); + $fcSetArray = $object->toArray(); $constraint_field = $object->get('constraint_field'); $constraint = $object->get('constraint'); @@ -95,16 +99,14 @@ public function prepareRow(xPDOObject $object) if ($constraint === '') { $constraint = "'{$constraint}'"; } - $objectArray['constraint_data'] = $object->get('constraint_class') . '.' . $constraint_field . ' = ' . $constraint; - } - $objectArray['perm'] = []; - if ($this->canEdit) { - $objectArray['perm'][] = 'pedit'; - } - if ($this->canRemove) { - $objectArray['perm'][] = 'premove'; + $fcSetArray['constraint_data'] = $object->get('constraint_class') . '.' . $constraint_field . ' = ' . $constraint; } + $fcSetArray['permissions'] = [ + 'create' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; - return $objectArray; + return $fcSetArray; } } diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index 1c3aeacddc..de7bd1c0c0 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -1760,19 +1760,36 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { * (uses the checkbox selection model to select multiple rows) * @param {Boolean} hasObjectLevelPermissions Whether individual rows might have * differing permissions, based on the specific object they represent + * @param {Boolean} markActiveRows Whether classes should be added for objects + * whose records that can be activated or deactivated (e.g., Form Customization, Users, etc.) * @returns {Object} The complete view config */ - getViewConfig: function(hasBulkActions = true, hasObjectLevelPermissions = true) { + getViewConfig: function(hasBulkActions = true, hasObjectLevelPermissions = true, markActiveRows = false) { return { forceFit: true, scrollOffset: 0, getRowClass: function(record, index, rowParams, store) { - // Adds the returned class to the row container's css classes - if (hasObjectLevelPermissions && this.grid.userCanDeleteRecord(record)) { - return ''; + const + canDeleteRecord = this.grid.userCanDeleteRecord(record), + rowClasses = [] + ; + // Objects whose records can be activated/deactivated do not depend upon permission to delete + if (markActiveRows && Object.hasOwn(record.data, 'active')) { + const activeClass = record.data.active ? 'grid-row-active' : 'grid-row-inactive'; + rowClasses.push(activeClass); + } + // Early return if no deletion restrictions are in effect + if (hasObjectLevelPermissions && canDeleteRecord) { + return rowClasses.length ? rowClasses.join(' ') : '' ; + } + // Add various classes marking a row as protected + if (hasBulkActions && !canDeleteRecord) { + rowClasses.push('disable-selection'); + } + if (record.json.isProtected) { + rowClasses.push('modx-protected-row'); } - const rowClasses = hasBulkActions ? 'disable-selection' : '' ; - return record.json.isProtected ? `modx-protected-row ${rowClasses}` : rowClasses ; + return rowClasses.length ? rowClasses.join(' ') : '' ; } }; } diff --git a/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js b/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js index c2fd2e7e84..215cf390ce 100644 --- a/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js +++ b/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js @@ -59,8 +59,7 @@ MODx.grid.FCProfile = function(config = {}) { 'usergroups', 'active', 'rank', - 'sets', - 'perm' + 'sets' ], paging: true, autosave: true, @@ -97,52 +96,37 @@ MODx.grid.FCProfile = function(config = {}) { dataIndex: 'usergroups', width: 150 }], - viewConfig: { - forceFit: true, - enableRowBody: true, - scrollOffset: 0, - autoFill: true, - showPreview: true, - getRowClass: function(rec, ri, p) { - return rec.data.active ? 'grid-row-active' : 'grid-row-inactive'; - } - }, tbar: [ { text: _('create'), scope: this, handler: this.createProfile, cls: 'primary-button' - }, { - text: _('bulk_actions'), - menu: [{ - text: _('selected_activate'), - handler: this.activateSelected, - scope: this - }, { - text: _('selected_deactivate'), - handler: this.deactivateSelected, - scope: this - }, { - text: _('selected_remove'), - handler: this.removeSelected, - scope: this - }] }, + this.getBulkActionsButton('profile', 'Security/Forms/Profile/RemoveMultiple', 'int', 'activate', 'deactivate'), '->', this.getQueryFilterField(), this.getClearFiltersButton() - ] + ], + viewConfig: this.getViewConfig(true, false, true) }); MODx.grid.FCProfile.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate']; + + // Note there are currently no action-specific, object-specific permissions for FC Profiles + this.setUserCanEdit(['customize_forms', 'save']); + this.setUserCanCreate(['customize_forms', 'save']); + this.setUserCanDelete(['customize_forms', 'remove']); + this.setShowActionsMenu(); + this.on('render', function() { this.getStore().reload(); }, this); }; Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { getMenu: function() { const record = this.getSelectionModel().getSelected(), - menu = [], - p = record.data.perm + menu = [] ; if (this.getSelectionModel().getCount() > 1) { menu.push({ @@ -153,13 +137,15 @@ Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { text: _('selected_deactivate'), handler: this.deactivateSelected }); - menu.push('-'); - menu.push({ - text: _('selected_remove'), - handler: this.removeSelected - }); + if (this.userCanDelete) { + menu.push('-'); + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected + }); + } } else { - if (p.indexOf('pedit') !== -1) { + if (this.userCanEdit) { menu.push({ text: _('edit'), handler: this.updateProfile @@ -179,7 +165,7 @@ Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { }); } } - if (p.indexOf('premove') !== -1) { + if (this.userCanDelete) { menu.push('-', { text: _('delete'), handler: this.confirm.createDelegate(this, ['Security/Forms/Profile/Remove', 'profile_remove_confirm']) @@ -198,9 +184,7 @@ Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { xtype: 'modx-window-fc-profile-create', listeners: { success: { - fn: function(r) { - this.refresh(); - }, + fn: this.refresh, scope: this } } @@ -309,32 +293,6 @@ Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { } }); return true; - }, - - removeSelected: function() { - const selections = this.getSelectedAsList(); - if (selections === false) { - return false; - } - MODx.msg.confirm({ - title: _('selected_remove'), - text: _('profile_remove_multiple_confirm'), - url: this.config.url, - params: { - action: 'Security/Forms/Profile/RemoveMultiple', - profiles: selections - }, - listeners: { - success: { - fn: function() { - this.getSelectionModel().clearSelections(true); - this.refresh(); - }, - scope: this - } - } - }); - return true; } }); Ext.reg('modx-grid-fc-profile', MODx.grid.FCProfile); @@ -350,29 +308,29 @@ MODx.window.CreateFCProfile = function(config = {}) { title: _('create'), url: MODx.config.connector_url, action: 'Security/Forms/Profile/Create', + formDefaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, fields: [{ xtype: 'textfield', name: 'name', fieldLabel: _('name'), - id: 'modx-fccp-name', - allowBlank: false, - anchor: '100%' + allowBlank: false }, { xtype: 'textarea', name: 'description', - fieldLabel: _('description'), - id: 'modx-fccp-description', - anchor: '100%' + fieldLabel: _('description') }, { xtype: 'xcheckbox', boxLabel: _('active'), hideLabel: true, name: 'active', - id: 'modx-fccp-active', inputValue: 1, value: 1, - checked: true, - anchor: '100%' + checked: true }], keys: [] }); diff --git a/manager/assets/modext/widgets/fc/modx.grid.fcset.js b/manager/assets/modext/widgets/fc/modx.grid.fcset.js index 988d05b005..b7fd6d01ca 100644 --- a/manager/assets/modext/widgets/fc/modx.grid.fcset.js +++ b/manager/assets/modext/widgets/fc/modx.grid.fcset.js @@ -18,8 +18,7 @@ MODx.grid.FCSet = function(config = {}) { 'constraint', 'constraint_field', 'constraint_class', - 'rules', - 'perm' + 'rules' ], paging: true, autosave: true, @@ -84,38 +83,15 @@ MODx.grid.FCSet = function(config = {}) { renderer: true } }], - viewConfig: { - forceFit: true, - enableRowBody: true, - scrollOffset: 0, - autoFill: true, - showPreview: true, - getRowClass: function(rec, ri, p) { - return rec.data.active ? 'grid-row-active' : 'grid-row-inactive'; - } - }, tbar: [ { text: _('create'), cls: 'primary-button', scope: this, handler: this.createSet - }, { - text: _('bulk_actions'), - menu: [{ - text: _('selected_activate'), - handler: this.activateSelected, - scope: this - }, { - text: _('selected_deactivate'), - handler: this.deactivateSelected, - scope: this - }, { - text: _('selected_remove'), - handler: this.removeSelected, - scope: this - }] - }, { + }, + this.getBulkActionsButton('set', 'Security/Forms/Set/RemoveMultiple', 'int', 'activate', 'deactivate'), + { text: _('import'), handler: this.importSet, scope: this @@ -123,16 +99,24 @@ MODx.grid.FCSet = function(config = {}) { '->', this.getQueryFilterField(), this.getClearFiltersButton() - ] + ], + viewConfig: this.getViewConfig(true, false, true) }); MODx.grid.FCSet.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate']; + + // Note there are currently no action-specific, object-specific permissions for FC Sets + this.setUserCanEdit(['customize_forms', 'save']); + this.setUserCanCreate(['customize_forms', 'save']); + this.setUserCanDelete(['customize_forms', 'remove']); + this.setShowActionsMenu(); }; Ext.extend(MODx.grid.FCSet, MODx.grid.Grid, { getMenu: function() { const record = this.getSelectionModel().getSelected(), - menu = [], - p = record.data.perm + menu = [] ; if (this.getSelectionModel().getCount() > 1) { menu.push({ @@ -143,13 +127,15 @@ Ext.extend(MODx.grid.FCSet, MODx.grid.Grid, { text: _('selected_deactivate'), handler: this.deactivateSelected }); - menu.push('-'); - menu.push({ - text: _('selected_remove'), - handler: this.removeSelected - }); + if (this.userCanDelete) { + menu.push('-'); + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected + }); + } } else { - if (p.indexOf('pedit') !== -1) { + if (this.userCanEdit) { menu.push({ text: _('edit'), handler: this.updateSet @@ -175,7 +161,7 @@ Ext.extend(MODx.grid.FCSet, MODx.grid.Grid, { }); } } - if (p.indexOf('premove') !== -1) { + if (this.userCanDelete) { menu.push('-', { text: _('delete'), handler: this.confirm.createDelegate(this, ['Security/Forms/Set/Remove', 'set_remove_confirm']) @@ -349,32 +335,6 @@ Ext.extend(MODx.grid.FCSet, MODx.grid.Grid, { } }); return true; - }, - - removeSelected: function() { - const selections = this.getSelectedAsList(); - if (selections === false) { - return false; - } - MODx.msg.confirm({ - title: _('selected_remove'), - text: _('set_remove_multiple_confirm'), - url: this.config.url, - params: { - action: 'Security/Forms/Set/RemoveMultiple', - sets: selections - }, - listeners: { - success: { - fn: function() { - this.getSelectionModel().clearSelections(true); - this.refresh(); - }, - scope: this - } - } - }); - return true; } }); Ext.reg('modx-grid-fc-set', MODx.grid.FCSet); From 22461e77ce3201bbcf40c041c7e39870bac0e243 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 7 Nov 2024 20:50:10 -0500 Subject: [PATCH 16/39] Update modx.panel.fcset.js Formatting, code style changes only --- .../modext/widgets/fc/modx.panel.fcset.js | 826 +++++++++--------- 1 file changed, 435 insertions(+), 391 deletions(-) diff --git a/manager/assets/modext/widgets/fc/modx.panel.fcset.js b/manager/assets/modext/widgets/fc/modx.panel.fcset.js index c02841caf4..0503495b50 100644 --- a/manager/assets/modext/widgets/fc/modx.panel.fcset.js +++ b/manager/assets/modext/widgets/fc/modx.panel.fcset.js @@ -6,88 +6,94 @@ */ MODx.panel.FCSet = function(config = {}) { Ext.applyIf(config, { - url: MODx.config.connector_url - ,baseParams: { + url: MODx.config.connector_url, + baseParams: { action: 'Security/Forms/Set/Update' - } - ,id: 'modx-panel-fc-set' - ,class_key: 'MODX\\Revolution\\modFormCustomizationSet' - ,cls: 'container' - ,items: [this.getPageHeader(config), MODx.getPageStructure([{ - title: _('set_and_fields') - ,xtype: 'panel' - ,border: false - ,defaults: { border: false } - ,items: [{ - html: '

          '+_('set_msg')+'

          ' - ,id: 'modx-fcs-msg' - ,xtype: 'modx-description' - },{ - layout: 'form' - ,id: 'modx-fcs-form' - ,defaults: { - anchor: '100%' - ,msgTarget: 'under' - ,validationEvent: 'change' - ,validateOnBlur: false - } - ,cls: 'main-wrapper' - ,labelWidth: 150 - ,items: [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-fcs-id' - ,value: config.record.id || MODx.request.id - },{ - xtype: 'modx-combo-fc-action' - ,fieldLabel: _('action') - ,name: 'action_id' - ,hiddenName: 'action_id' - ,id: 'modx-fcs-action' - ,allowBlank: false - ,value: config.record.action - ,listeners: { - 'select': {scope:this,fn:function(f,e) { - Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getRawValue())); - }} - } - },{ - xtype: 'modx-combo-template' - ,fieldLabel: _('template') - ,description: _('set_template_desc') - ,name: 'template' - ,hiddenName: 'template' - ,value: config.record.template || 0 - ,lazyInit: false - ,lazyRender: false - ,baseParams: { - action: 'Element/Template/GetList' - ,combo: true + }, + id: 'modx-panel-fc-set', + class_key: 'MODX\\Revolution\\modFormCustomizationSet', + cls: 'container', + items: [this.getPageHeader(config), MODx.getPageStructure([{ + title: _('set_and_fields'), + xtype: 'panel', + border: false, + defaults: { border: false }, + items: [{ + html: `

          ${_('set_msg')}

          `, + id: 'modx-fcs-msg', + xtype: 'modx-description' + }, { + layout: 'form', + id: 'modx-fcs-form', + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + cls: 'main-wrapper', + labelWidth: 150, + items: [{ + xtype: 'hidden', + name: 'id', + id: 'modx-fcs-id', + value: config.record.id || MODx.request.id + }, { + xtype: 'modx-combo-fc-action', + fieldLabel: _('action'), + name: 'action_id', + hiddenName: 'action_id', + id: 'modx-fcs-action', + allowBlank: false, + value: config.record.action, + listeners: { + select: { + fn: function(f, e) { + Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getRawValue())); + }, + scope: this + } } - ,listeners: { - 'select': {fn:this.changeTemplate,scope:this} + }, { + xtype: 'modx-combo-template', + fieldLabel: _('template'), + description: _('set_template_desc'), + name: 'template', + hiddenName: 'template', + value: config.record.template || 0, + lazyInit: false, + lazyRender: false, + baseParams: { + action: 'Element/Template/GetList', + combo: true + }, + listeners: { + select: { + fn: this.changeTemplate, + scope: this + } } - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,id: 'modx-fcs-description' - ,maxLength: 255 - ,grow: false - ,value: config.record.description - },{ - xtype: 'hidden' - ,fieldLabel: _('constraint_class') - ,name: 'constraint_class' - ,value: 'MODX\\Revolution\\modResource' - ,allowBlank: true - },{ - xtype: 'textfield' - ,fieldLabel: _('constraint_field') - ,description: _('set_constraint_field_desc') - ,name: 'constraint_field' - ,value: config.record.constraint_field - ,listeners: { + }, { + xtype: 'textarea', + fieldLabel: _('description'), + name: 'description', + id: 'modx-fcs-description', + maxLength: 255, + grow: false, + value: config.record.description + }, { + xtype: 'hidden', + fieldLabel: _('constraint_class'), + name: 'constraint_class', + value: 'MODX\\Revolution\\modResource', + allowBlank: true + }, { + xtype: 'textfield', + fieldLabel: _('constraint_field'), + description: _('set_constraint_field_desc'), + name: 'constraint_field', + value: config.record.constraint_field, + listeners: { change: { fn: function(cmp, newValue, oldValue) { if (!Ext.isEmpty(newValue)) { @@ -100,13 +106,13 @@ MODx.panel.FCSet = function(config = {}) { scope: this } } - },{ - xtype: 'textfield' - ,fieldLabel: _('constraint') - ,description: _('set_constraint_desc') - ,name: 'constraint' - ,value: config.record.constraint - ,listeners: { + }, { + xtype: 'textfield', + fieldLabel: _('constraint'), + description: _('set_constraint_desc'), + name: 'constraint', + value: config.record.constraint, + listeners: { change: { fn: function(cmp, newValue, oldValue) { if (!Ext.isEmpty(newValue)) { @@ -119,125 +125,127 @@ MODx.panel.FCSet = function(config = {}) { scope: this } } - },{ - xtype: 'xcheckbox' - ,fieldLabel: _('active') - ,name: 'active' - ,inputValue: true - ,value: Boolean(config.record.active) + }, { + xtype: 'xcheckbox', + fieldLabel: _('active'), + name: 'active', + inputValue: true, + value: Boolean(config.record.active) }] - },{ - html: '

          '+_('set_fields_msg')+'

          ' - ,xtype: 'modx-description' - },{ - id: 'modx-fcs-fields-form' - ,msgTarget: 'side' - ,cls: 'main-wrapper' - ,layout: 'anchor' - ,items: [{ - xtype: 'modx-grid-fc-set-fields' - ,data: config.record.fields || [] - ,preventRender: true + }, { + html: `

          ${_('set_fields_msg')}

          `, + xtype: 'modx-description' + }, { + id: 'modx-fcs-fields-form', + msgTarget: 'side', + cls: 'main-wrapper', + layout: 'anchor', + items: [{ + xtype: 'modx-grid-fc-set-fields', + data: config.record.fields || [], + preventRender: true }] }] - },{ - title: _('regions') - ,border: false - ,layout: 'anchor' - ,items: [{ - html: '

          '+_('set_tabs_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-fc-set-tabs' - ,cls: 'main-wrapper' - ,data: config.record.tabs || [] - ,preventRender: true + }, { + title: _('regions'), + border: false, + layout: 'anchor', + items: [{ + html: `

          ${_('set_tabs_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-fc-set-tabs', + cls: 'main-wrapper', + data: config.record.tabs || [], + preventRender: true }] - },{ - title: _('tvs') - ,border: false - ,layout: 'anchor' - ,items: [{ - html: '

          '+_('set_tvs_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-fc-set-tvs' - ,cls: 'main-wrapper' - ,data: config.record.tvs || [] - ,preventRender: true + }, { + title: _('tvs'), + border: false, + layout: 'anchor', + items: [{ + html: `

          ${_('set_tvs_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-fc-set-tvs', + cls: 'main-wrapper', + data: config.record.tvs || [], + preventRender: true }] - }],{ + }], { id: 'modx-fc-set-tabs' - })] - ,listeners: { - 'setup': {fn:this.setup,scope:this} - ,'success': {fn:this.success,scope:this} - ,'beforeSubmit': {fn:this.beforeSubmit,scope:this} + })], + listeners: { + setup: { fn: this.setup, scope: this }, + success: { fn: this.success, scope: this }, + beforeSubmit: { fn: this.beforeSubmit, scope: this } } }); - MODx.panel.FCSet.superclass.constructor.call(this,config); + MODx.panel.FCSet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.FCSet,MODx.FormPanel,{ - initialized: false +Ext.extend(MODx.panel.FCSet, MODx.FormPanel, { + initialized: false, - ,setup: function() { - if (!this.initialized) {this.getForm().setValues(this.config.record);} + setup: function() { + if (!this.initialized) { + this.getForm().setValues(this.config.record); + } Ext.getCmp('modx-header-breadcrumbs').updateHeader(_('set')); - this.fireEvent('ready',this.config.record); + this.fireEvent('ready', this.config.record); this.clearDirty(); this.initialized = true; MODx.fireEvent('ready'); return true; - } + }, - ,beforeSubmit: function(o) { - Ext.apply(o.form.baseParams,{ - fields: Ext.getCmp('modx-grid-fc-set-fields').encode() - ,tabs: Ext.getCmp('modx-grid-fc-set-tabs').encode() - ,tvs: Ext.getCmp('modx-grid-fc-set-tvs').encode() + beforeSubmit: function(o) { + Ext.apply(o.form.baseParams, { + fields: Ext.getCmp('modx-grid-fc-set-fields').encode(), + tabs: Ext.getCmp('modx-grid-fc-set-tabs').encode(), + tvs: Ext.getCmp('modx-grid-fc-set-tvs').encode() }); - return this.fireEvent('save',{ + return this.fireEvent('save', { values: this.getForm().getValues() }); - } + }, - ,success: function(r) { + success: function(r) { this.getForm().setValues(r.result.object); Ext.getCmp('modx-grid-fc-set-fields').getStore().commitChanges(); Ext.getCmp('modx-grid-fc-set-tabs').getStore().commitChanges(); Ext.getCmp('modx-grid-fc-set-tvs').getStore().commitChanges(); - } + }, - ,changeTemplate: function(cb) { - if (cb.getValue() != this.config.record.template) { - Ext.Msg.confirm(_('set_change_template'),_('set_change_template_confirm'),function(e) { - if (e == 'yes') { - this.on('success',function() { - location.href = location.href; - },this); + changeTemplate: function(cb) { + if (cb.getValue() !== this.config.record.template) { + Ext.Msg.confirm(_('set_change_template'), _('set_change_template_confirm'), function(e) { + if (e === 'yes') { + this.on('success', function() { + window.location.reload(); + }, this); this.submit(); } else { cb.setValue(this.config.record.template); } - },this); + }, this); } return false; - } + }, - ,getPageHeader: function(config) { - var profile = config.record.profile; + getPageHeader: function(config) { + const { profile } = config.record; return MODx.util.getHeaderBreadCrumbs('modx-fcs-header', [{ text: _('form_customization'), href: MODx.getPage('security/forms') - },{ + }, { text: _('profile'), - href: MODx.getPage('security/forms/profile/update&id='+profile) + href: MODx.getPage(`security/forms/profile/update&id=${profile}`) }]); } }); -Ext.reg('modx-panel-fc-set',MODx.panel.FCSet); +Ext.reg('modx-panel-fc-set', MODx.panel.FCSet); /** * @class MODx.grid.FCSetFields @@ -245,71 +253,81 @@ Ext.reg('modx-panel-fc-set',MODx.panel.FCSet); * @param {Object} config An object of configuration properties * @xtype modx-grid-fc-set-fields */ -MODx.grid.FCSetFields = function(config) { - config = config || {}; +MODx.grid.FCSetFields = function(config = {}) { this.vcb = new Ext.ux.grid.CheckColumn({ - header: _('visible') - ,dataIndex: 'visible' - ,width: 40 - ,sortable: false + header: _('visible'), + dataIndex: 'visible', + width: 40, + sortable: false }); - Ext.applyIf(config,{ - id: 'modx-grid-fc-set-fields' - ,showActionsColumn: false - ,fields: ['id','action','name','tab','tab_rank','other','rank','visible','label','default_value'] - ,autoHeight: true - ,grouping: true - ,groupBy: 'tab_rank' - ,plugins: [this.vcb] - ,stateful: false - ,remoteSort: false - ,sortBy: 'rank' - ,sortDir: 'ASC' - ,hideGroupedColumn: true - ,groupTextTpl: '{[values.rs[0].data.tab]} ({[values.rs.length]} {[values.rs.length > 1 ? "'+_('fields')+'" : "'+_('field')+'"]})' - ,columns: [{ - header: _('name') - ,dataIndex: 'name' - ,width: 200 - },{ - header: _('region')+' ('+_('tab_id')+')' - ,dataIndex: 'tab' - ,width: 100 - },{ - header: _('tab_rank') - ,dataIndex: 'tab_rank' - ,width: 100 - },this.vcb,{ - header: _('label') - ,dataIndex: 'label' - ,editor: { xtype: 'textfield' } - ,renderer: function(v,md) { - return Ext.util.Format.htmlEncode(v); + Ext.applyIf(config, { + id: 'modx-grid-fc-set-fields', + showActionsColumn: false, + fields: [ + 'id', + 'action', + 'name', + 'tab', + 'tab_rank', + 'other', + 'rank', + 'visible', + 'label', + 'default_value' + ], + autoHeight: true, + grouping: true, + groupBy: 'tab_rank', + plugins: [this.vcb], + stateful: false, + remoteSort: false, + sortBy: 'rank', + sortDir: 'ASC', + hideGroupedColumn: true, + groupTextTpl: `{[values.rs[0].data.tab]} ({[values.rs.length]} {[values.rs.length > 1 ? "${_('fields')}" : "${_('field')}"]})`, + columns: [{ + header: _('name'), + dataIndex: 'name', + width: 200 + }, { + header: `${_('region')} (${_('tab_id')})`, + dataIndex: 'tab', + width: 100 + }, { + header: _('tab_rank'), + dataIndex: 'tab_rank', + width: 100 + }, this.vcb, { + header: _('label'), + dataIndex: 'label', + editor: { xtype: 'textfield' }, + renderer: function(value, metaData) { + return Ext.util.Format.htmlEncode(value); } - },{ - header: _('default_value') - ,dataIndex: 'default_value' - ,editor: { xtype: 'textfield' } - ,renderer: function(v,md) { + }, { + header: _('default_value'), + dataIndex: 'default_value', + editor: { xtype: 'textfield' }, + renderer: function(v, md) { return Ext.util.Format.htmlEncode(v); } - }] - ,viewConfig: { - forceFit:true - ,enableRowBody:true - ,scrollOffset: 0 - ,autoFill: true - ,showPreview: true - ,getRowClass : function(rec, ri, p){ - return rec.data.visible ? 'grid-row-active' : 'grid-row-inactive'; + }], + viewConfig: { + forceFit: true, + enableRowBody: true, + scrollOffset: 0, + autoFill: true, + showPreview: true, + getRowClass: function(record, rowIndex, rowParams, store) { + return record.data.visible ? 'grid-row-active' : 'grid-row-inactive'; } } }); - MODx.grid.FCSetFields.superclass.constructor.call(this,config); + MODx.grid.FCSetFields.superclass.constructor.call(this, config); this.propRecord = Ext.data.Record.create(config.fields); }; -Ext.extend(MODx.grid.FCSetFields,MODx.grid.LocalGrid); -Ext.reg('modx-grid-fc-set-fields',MODx.grid.FCSetFields); +Ext.extend(MODx.grid.FCSetFields, MODx.grid.LocalGrid); +Ext.reg('modx-grid-fc-set-fields', MODx.grid.FCSetFields); /** * @class MODx.grid.FCSetTabs @@ -317,90 +335,104 @@ Ext.reg('modx-grid-fc-set-fields',MODx.grid.FCSetFields); * @param {Object} config An object of configuration properties * @xtype modx-grid-fc-set-tabs */ -MODx.grid.FCSetTabs = function(config) { - config = config || {}; +MODx.grid.FCSetTabs = function(config = {}) { this.vcb = new Ext.ux.grid.CheckColumn({ - header: _('visible') - ,dataIndex: 'visible' - ,width: 40 - ,sortable: false + header: _('visible'), + dataIndex: 'visible', + width: 40, + sortable: false }); - Ext.applyIf(config,{ - id: 'modx-grid-fc-set-tabs' - ,showActionsColumn: false - ,fields: ['id','action','name','form','other','rank','visible','label','type'] - ,autoHeight: true - ,plugins: [this.vcb] - ,stateful: false - ,columns: [{ - header: _('region')+' ('+_('tab_id')+')' - ,dataIndex: 'name' - ,width: 100 - },this.vcb,{ - header: _('tab_title') - ,dataIndex: 'label' - ,editor: { xtype: 'textfield' } - }] - ,viewConfig: { - forceFit:true - ,enableRowBody:true - ,scrollOffset: 0 - ,autoFill: true - ,showPreview: true - ,getRowClass : function(rec, ri, p){ - return rec.data.visible ? 'grid-row-active' : 'grid-row-inactive'; + Ext.applyIf(config, { + id: 'modx-grid-fc-set-tabs', + showActionsColumn: false, + fields: [ + 'id', + 'action', + 'name', + 'form', + 'other', + 'rank', + 'visible', + 'label', + 'type' + ], + autoHeight: true, + plugins: [this.vcb], + stateful: false, + columns: [{ + header: `${_('region')} (${_('tab_id')})`, + dataIndex: 'name', + width: 100 + }, this.vcb, { + header: _('tab_title'), + dataIndex: 'label', + editor: { xtype: 'textfield' } + }], + viewConfig: { + forceFit: true, + enableRowBody: true, + scrollOffset: 0, + autoFill: true, + showPreview: true, + getRowClass: function(record, rowIndex, rowParams, store) { + return record.data.visible ? 'grid-row-active' : 'grid-row-inactive'; } - } - ,tbar: [{ - text: _('create') - ,cls: 'primary-button' - ,handler: this.createTab - ,scope: this + }, + tbar: [{ + text: _('create'), + cls: 'primary-button', + handler: this.createTab, + scope: this }] }); - MODx.grid.FCSetTabs.superclass.constructor.call(this,config); + MODx.grid.FCSetTabs.superclass.constructor.call(this, config); this.propRecord = Ext.data.Record.create(config.fields); }; -Ext.extend(MODx.grid.FCSetTabs,MODx.grid.LocalGrid,{ - getMenu: function(g,ri) { - var rec = this.getStore().getAt(ri); - if (rec.data.type == 'new') { +Ext.extend(MODx.grid.FCSetTabs, MODx.grid.LocalGrid, { + getMenu: function(g, ri) { + const record = this.getStore().getAt(ri); + if (record.data.type === 'new') { return [{ - text: _('delete') - ,handler: this.removeTab - ,scope: this + text: _('delete'), + handler: this.removeTab, + scope: this }]; } return []; - } + }, - ,createTab: function(btn,e) { + createTab: function(btn, e) { if (!this.windows.ctab) { this.windows.ctab = MODx.load({ - xtype: 'modx-window-fc-set-add-tab' - ,listeners: { - 'success': {fn:function(r) { - var s = this.getStore(); - var rec = new this.propRecord(r); - s.add(rec); - },scope:this} + xtype: 'modx-window-fc-set-add-tab', + listeners: { + success: { + fn: function(response) { + const + store = this.getStore(), + record = new this.propRecord(response) + ; + store.add(record); + }, + scope: this + } } }); } this.windows.ctab.reset(); this.windows.ctab.show(e.target); - } + }, - ,removeTab: function(btn,e) { - var rec = this.getSelectionModel().getSelected(); - Ext.Msg.confirm(_('delete'),_('tab_remove_confirm'),function(e) { - if (e == 'yes') { - this.getStore().remove(rec); + removeTab: function(btn, e) { + const record = this.getSelectionModel().getSelected(); + Ext.Msg.confirm(_('delete'), _('tab_remove_confirm'), function(e) { + if (e === 'yes') { + this.getStore().remove(record); } - },this); + }, this); } }); -Ext.reg('modx-grid-fc-set-tabs',MODx.grid.FCSetTabs); +Ext.reg('modx-grid-fc-set-tabs', MODx.grid.FCSetTabs); /** * @class MODx.grid.FCSetTVs @@ -408,81 +440,93 @@ Ext.reg('modx-grid-fc-set-tabs',MODx.grid.FCSetTabs); * @param {Object} config An object of configuration properties * @xtype modx-grid-fc-set-tvs */ -MODx.grid.FCSetTVs = function(config) { - config = config || {}; +MODx.grid.FCSetTVs = function(config = {}) { this.vcb = new Ext.ux.grid.CheckColumn({ - header: _('visible') - ,dataIndex: 'visible' - ,width: 40 - ,sortable: false + header: _('visible'), + dataIndex: 'visible', + width: 40, + sortable: false }); - Ext.applyIf(config,{ - id: 'modx-grid-fc-set-tvs' - ,showActionsColumn: false - ,fields: ['id','name','tab','rank','visible','label','default_value','category','default_text'] - ,autoHeight: true - ,grouping: true - ,groupBy: 'category' - ,sortBy: 'rank' - ,sortDir: 'ASC' - ,stateful: false - ,groupTextTpl: '{group} ({[values.rs.length]} {[values.rs.length > 1 ? "'+_('tvs')+'" : "'+_('tv')+'"]})' - ,plugins: [this.vcb] - ,hideGroupedColumn: true - ,columns: [{ - header: _('category') - ,dataIndex: 'category' - },{ - header: _('tv_name') - ,dataIndex: 'name' - ,width: 200 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=element/tv/update&id=' + record.data.id - ,target: '_blank' - }); - }, scope: this } - },this.vcb,{ - header: _('label') - ,dataIndex: 'label' - ,editor: { xtype: 'textfield' } - },{ - header: _('default_value') - ,dataIndex: 'default_value' - ,editor: { xtype: 'textfield' } - ,renderer: function(v) { return Ext.util.Format.htmlEncode(v); } - },{ - header: _('original_value') - ,dataIndex: 'default_text' - ,editable: false - },{ - header: _('region')+' ('+_('tab_id')+')' - ,dataIndex: 'tab' - ,width: 100 - ,editor: { xtype: 'textfield' } - },{ - header: _('tab_rank') - ,dataIndex: 'rank' - ,width: 70 - ,editor: { xtype: 'textfield' } - }] - ,viewConfig: { - forceFit:true - ,enableRowBody:true - ,scrollOffset: 0 - ,autoFill: true - ,showPreview: true - ,getRowClass : function(rec, ri, p){ - return rec.data.visible ? 'grid-row-active' : 'grid-row-inactive'; + Ext.applyIf(config, { + id: 'modx-grid-fc-set-tvs', + showActionsColumn: false, + fields: [ + 'id', + 'name', + 'tab', + 'rank', + 'visible', + 'label', + 'default_value', + 'category', + 'default_text' + ], + autoHeight: true, + grouping: true, + groupBy: 'category', + sortBy: 'rank', + sortDir: 'ASC', + stateful: false, + groupTextTpl: `{group} ({[values.rs.length]} {[values.rs.length > 1 ? "${_('tvs')}" : "${_('tv')}"]})`, + plugins: [this.vcb], + hideGroupedColumn: true, + columns: [{ + header: _('category'), + dataIndex: 'category' + }, { + header: _('tv_name'), + dataIndex: 'name', + width: 200, + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=element/tv/update&id=${record.data.id}`, + target: '_blank' + }); + }, + scope: this + } + }, this.vcb, { + header: _('label'), + dataIndex: 'label', + editor: { xtype: 'textfield' } + }, { + header: _('default_value'), + dataIndex: 'default_value', + editor: { xtype: 'textfield' }, + renderer: function(v) { return Ext.util.Format.htmlEncode(v); } + }, { + header: _('original_value'), + dataIndex: 'default_text', + editable: false + }, { + header: `${_('region')} (${_('tab_id')})`, + dataIndex: 'tab', + width: 100, + editor: { xtype: 'textfield' } + }, { + header: _('tab_rank'), + dataIndex: 'rank', + width: 70, + editor: { xtype: 'textfield' } + }], + viewConfig: { + forceFit: true, + enableRowBody: true, + scrollOffset: 0, + autoFill: true, + showPreview: true, + getRowClass: function(record, rowIndex, rowParams, store) { + return record.data.visible ? 'grid-row-active' : 'grid-row-inactive'; } } }); - MODx.grid.FCSetTVs.superclass.constructor.call(this,config); + MODx.grid.FCSetTVs.superclass.constructor.call(this, config); this.propRecord = Ext.data.Record.create(config.fields); }; -Ext.extend(MODx.grid.FCSetTVs,MODx.grid.LocalGrid,{ +Ext.extend(MODx.grid.FCSetTVs, MODx.grid.LocalGrid, { }); -Ext.reg('modx-grid-fc-set-tvs',MODx.grid.FCSetTVs); +Ext.reg('modx-grid-fc-set-tvs', MODx.grid.FCSetTVs); /** * @class MODx.window.AddTabToSet @@ -490,58 +534,58 @@ Ext.reg('modx-grid-fc-set-tvs',MODx.grid.FCSetTVs); * @param {Object} config An object of options. * @xtype modx-window-fc-set-add-tab */ -MODx.window.AddTabToSet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('create') - ,fields: [{ - xtype: 'hidden' - ,name: 'container' - ,value: 'modx-resource-tabs' - },{ - xtype: 'hidden' - ,name: 'visible' - ,value: true - },{ - xtype: 'hidden' - ,name: 'type' - ,value: 'new' - },{ - xtype: 'textfield' - ,name: 'name' - ,fieldLabel: _('region')+' ('+_('tab_id')+')' - ,id: 'modx-fcatab-id' - ,allowBlank: false - ,anchor: '100%' - },{ - xtype: 'textfield' - ,fieldLabel: _('tab_title') - ,name: 'value' - ,id: 'modx-fcatab-name' - ,allowBlank: false - ,anchor: '100%' +MODx.window.AddTabToSet = function(config = {}) { + Ext.applyIf(config, { + title: _('create'), + fields: [{ + xtype: 'hidden', + name: 'container', + value: 'modx-resource-tabs' + }, { + xtype: 'hidden', + name: 'visible', + value: true + }, { + xtype: 'hidden', + name: 'type', + value: 'new' + }, { + xtype: 'textfield', + name: 'name', + fieldLabel: `${_('region')} (${_('tab_id')})`, + id: 'modx-fcatab-id', + allowBlank: false, + anchor: '100%' + }, { + xtype: 'textfield', + fieldLabel: _('tab_title'), + name: 'value', + id: 'modx-fcatab-name', + allowBlank: false, + anchor: '100%' }] }); - MODx.window.AddTabToSet.superclass.constructor.call(this,config); + MODx.window.AddTabToSet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.AddTabToSet,MODx.Window,{ +Ext.extend(MODx.window.AddTabToSet, MODx.Window, { submit: function() { - var rec = this.fp.getForm().getValues(); - - var g = Ext.getCmp('modx-grid-fc-set-tabs'); - var s = g.getStore(); - var v = s.query('name',rec.name).items; - if (v.length > 0) { - MODx.msg.alert(_('error'),_('set_tab_err_ae')); + const + record = this.fp.getForm().getValues(), + grid = Ext.getCmp('modx-grid-fc-set-tabs'), + store = grid.getStore(), + matches = store.query('name', record.name).items + ; + if (matches.length > 0) { + MODx.msg.alert(_('error'), _('set_tab_err_ae')); return false; } - rec.label = rec.value; - rec.visible = true; - rec.type = 'new'; + record.label = record.value; + record.visible = true; + record.type = 'new'; - this.fireEvent('success',rec); + this.fireEvent('success', record); this.hide(); return false; } }); -Ext.reg('modx-window-fc-set-add-tab',MODx.window.AddTabToSet); +Ext.reg('modx-window-fc-set-add-tab', MODx.window.AddTabToSet); From 6a6fded7430181c9a1e0560892d4eff40d01826c Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 7 Nov 2024 22:26:33 -0500 Subject: [PATCH 17/39] Update modx.panel.fcprofile.js Formatting, code style changes only --- .../modext/widgets/fc/modx.panel.fcprofile.js | 353 +++++++++--------- 1 file changed, 181 insertions(+), 172 deletions(-) diff --git a/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js b/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js index 002b5e6ed8..cbef0c73e5 100644 --- a/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js +++ b/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js @@ -4,140 +4,141 @@ * @param {Object} config An object of configuration properties * @xtype modx-panel-fc-profile */ -MODx.panel.FCProfile = function(config) { - config = config || {}; - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { +MODx.panel.FCProfile = function(config = {}) { + Ext.applyIf(config, { + url: MODx.config.connector_url, + baseParams: { action: 'Security/Forms/Profile/Update' - } - ,id: 'modx-panel-fc-profile' - ,cls: 'container' - ,class_key: 'MODX\\Revolution\\modFormCustomizationProfile' - ,bodyStyle: '' - ,items: [this.getPageHeader(config), MODx.getPageStructure([{ - title: _('profile') - ,defaults: { border: false ,msgTarget: 'side' } - ,layout: 'form' - ,id: 'modx-fcp-form' - ,labelWidth: 150 - ,items: [{ - html: '

          '+_('profile_msg')+'

          ' - ,id: 'modx-fcp-msg' - ,xtype: 'modx-description' - },{ - xtype: 'panel' - ,border: false - ,cls:'main-wrapper' - ,layout: 'form' - ,items: [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-fcp-id' - ,value: config.record.id || MODx.request.id - },{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'name' - ,id: 'modx-fcp-name' - ,anchor: '100%' - ,maxLength: 191 - ,enableKeyEvents: true - ,allowBlank: false - ,value: config.record.name - ,listeners: { - 'keyup': {scope:this,fn:function(f,e) { - Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getValue())); - }} + }, + id: 'modx-panel-fc-profile', + cls: 'container', + class_key: 'MODX\\Revolution\\modFormCustomizationProfile', + bodyStyle: '', + items: [this.getPageHeader(config), MODx.getPageStructure([{ + title: _('profile'), + defaults: { border: false, msgTarget: 'side' }, + layout: 'form', + id: 'modx-fcp-form', + labelWidth: 150, + items: [{ + html: `

          ${_('profile_msg')}

          `, + id: 'modx-fcp-msg', + xtype: 'modx-description' + }, { + xtype: 'panel', + border: false, + cls: 'main-wrapper', + layout: 'form', + items: [{ + xtype: 'hidden', + name: 'id', + id: 'modx-fcp-id', + value: config.record.id || MODx.request.id + }, { + xtype: 'textfield', + fieldLabel: _('name'), + name: 'name', + id: 'modx-fcp-name', + anchor: '100%', + maxLength: 191, + enableKeyEvents: true, + allowBlank: false, + value: config.record.name, + listeners: { + keyup: { + fn: function(f, e) { + Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(f.getValue())); + }, + scope: this + } } - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,id: 'modx-fcp-description' - ,anchor: '100%' - ,maxLength: 255 - ,grow: false - ,value: config.record.description - },{ - xtype: 'xcheckbox' - ,fieldLabel: _('active') - ,name: 'active' - ,id: 'modx-fcp-active' - ,inputValue: true - ,value: config.record.active ? true : false - ,anchor: '100%' - ,allowBlank: true + }, { + xtype: 'textarea', + fieldLabel: _('description'), + name: 'description', + id: 'modx-fcp-description', + anchor: '100%', + maxLength: 255, + grow: false, + value: config.record.description + }, { + xtype: 'xcheckbox', + fieldLabel: _('active'), + name: 'active', + id: 'modx-fcp-active', + inputValue: true, + value: Boolean(config.record.active), + anchor: '100%' }] - },{ - xtype: 'modx-grid-fc-set' - ,cls:'main-wrapper' - ,baseParams: { - action: 'Security/Forms/Set/GetList' - ,profile: config.record.id - } - ,preventRender: true + }, { + xtype: 'modx-grid-fc-set', + cls: 'main-wrapper', + baseParams: { + action: 'Security/Forms/Set/GetList', + profile: config.record.id + }, + preventRender: true }] - },{ - title: _('usergroups') - ,layout: 'anchor' - ,items: [{ - html: '

          '+_('profile_usergroups_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-fc-profile-usergroups' - ,cls:'main-wrapper' - ,data: config.record.usergroups || [] - ,preventRender: true + }, { + title: _('usergroups'), + layout: 'anchor', + items: [{ + html: `

          ${_('profile_usergroups_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-fc-profile-usergroups', + cls: 'main-wrapper', + data: config.record.usergroups || [], + preventRender: true }] - }],{ + }], { id: 'modx-fc-profile-tabs' - })] - ,listeners: { - 'setup': {fn:this.setup,scope:this} - ,'success': {fn:this.success,scope:this} - ,'beforeSubmit': {fn:this.beforeSubmit,scope:this} + })], + listeners: { + setup: { fn: this.setup, scope: this }, + success: { fn: this.success, scope: this }, + beforeSubmit: { fn: this.beforeSubmit, scope: this } } }); - MODx.panel.FCProfile.superclass.constructor.call(this,config); + MODx.panel.FCProfile.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.FCProfile,MODx.FormPanel,{ - initialized: false +Ext.extend(MODx.panel.FCProfile, MODx.FormPanel, { + initialized: false, - ,setup: function() { + setup: function() { if (!this.initialized) { this.getForm().setValues(this.config.record); } if (!Ext.isEmpty(this.config.record.name)) { Ext.getCmp('modx-header-breadcrumbs').updateHeader(Ext.util.Format.htmlEncode(this.config.record.name)); } - this.fireEvent('ready',this.config.record); + this.fireEvent('ready', this.config.record); this.clearDirty(); this.initialized = true; MODx.fireEvent('ready'); return true; - } + }, - ,beforeSubmit: function(o) { - Ext.apply(o.form.baseParams,{ + beforeSubmit: function(o) { + Ext.apply(o.form.baseParams, { usergroups: Ext.getCmp('modx-grid-fc-profile-usergroups').encode() }); - return this.fireEvent('save',{ + return this.fireEvent('save', { values: this.getForm().getValues() }); - } + }, - ,success: function(r) { + success: function(r) { Ext.getCmp('modx-grid-fc-profile-usergroups').getStore().commitChanges(); this.getForm().setValues(r.result.object); - } + }, - ,getPageHeader: function(config) { + getPageHeader: function(config) { return MODx.util.getHeaderBreadCrumbs('modx-fcp-header', [{ text: _('form_customization'), href: MODx.getPage('security/forms') }]); } }); -Ext.reg('modx-panel-fc-profile',MODx.panel.FCProfile); +Ext.reg('modx-panel-fc-profile', MODx.panel.FCProfile); /** * @class MODx.grid.FCProfileUserGroups @@ -145,65 +146,72 @@ Ext.reg('modx-panel-fc-profile',MODx.panel.FCProfile); * @param {Object} config An object of configuration properties * @xtype modx-grid-fc-profile-usergroups */ -MODx.grid.FCProfileUserGroups = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-grid-fc-profile-usergroups' - ,fields: ['id','name'] - ,autoHeight: true - ,stateful: false - ,columns: [{ - header: _('name') - ,dataIndex: 'name' - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/usergroup/update&id=' + record.data.id - ,target: '_blank' - }); - }, scope: this } - }] - ,tbar: [{ - text: _('usergroup_create') - ,cls: 'primary-button' - ,handler: this.addUserGroup - ,scope: this +MODx.grid.FCProfileUserGroups = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-grid-fc-profile-usergroups', + fields: ['id', 'name'], + autoHeight: true, + stateful: false, + columns: [{ + header: _('name'), + dataIndex: 'name', + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=security/usergroup/update&id=${record.data.id}`, + target: '_blank' + }); + }, + scope: this + } + }], + tbar: [{ + text: _('usergroup_create'), + cls: 'primary-button', + handler: this.addUserGroup, + scope: this }] }); - MODx.grid.FCProfileUserGroups.superclass.constructor.call(this,config); + MODx.grid.FCProfileUserGroups.superclass.constructor.call(this, config); this.fcugRecord = Ext.data.Record.create(config.fields); }; -Ext.extend(MODx.grid.FCProfileUserGroups,MODx.grid.LocalGrid,{ - getMenu: function(g,ri) { +Ext.extend(MODx.grid.FCProfileUserGroups, MODx.grid.LocalGrid, { + getMenu: function(g, ri) { return [{ - text: _('usergroup_remove') - ,handler: this.removeUserGroup - ,scope: this + text: _('usergroup_remove'), + handler: this.removeUserGroup, + scope: this }]; - } + }, - ,addUserGroup: function(btn,e) { - this.loadWindow(btn,e,{ - xtype: 'modx-window-fc-profile-add-usergroup' - ,listeners: { - 'success': {fn:function(r) { - var s = this.getStore(); - var rec = new this.fcugRecord(r); - s.add(rec); - },scope:this} + addUserGroup: function(btn, e) { + this.loadWindow(btn, e, { + xtype: 'modx-window-fc-profile-add-usergroup', + listeners: { + success: { + fn: function(response) { + const + store = this.getStore(), + record = new this.fcugRecord(response) + ; + store.add(record); + }, + scope: this + } } }); - } + }, - ,removeUserGroup: function(btn,e) { - var rec = this.getSelectionModel().getSelected(); - Ext.Msg.confirm(_('usergroup_remove'),_('usergroup_remove_confirm'),function(e) { - if (e == 'yes') { - this.getStore().remove(rec); + removeUserGroup: function(btn, e) { + const record = this.getSelectionModel().getSelected(); + Ext.Msg.confirm(_('usergroup_remove'), _('usergroup_remove_confirm'), function(e) { + if (e === 'yes') { + this.getStore().remove(record); } - },this); + }, this); } }); -Ext.reg('modx-grid-fc-profile-usergroups',MODx.grid.FCProfileUserGroups); +Ext.reg('modx-grid-fc-profile-usergroups', MODx.grid.FCProfileUserGroups); /** * @class MODx.window.AddGroupToProfile @@ -211,40 +219,41 @@ Ext.reg('modx-grid-fc-profile-usergroups',MODx.grid.FCProfileUserGroups); * @param {Object} config An object of options. * @xtype modx-window-fc-profile-add-usergroup */ -MODx.window.AddGroupToProfile = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('usergroup_create') - ,fields: [{ - fieldLabel: _('user_group') - ,name: 'usergroup' - ,hiddenName: 'usergroup' - ,id: 'modx-fcaug-usergroup' - ,xtype: 'modx-combo-usergroup' - ,editable: false - ,allowBlank: false - ,anchor: '100%' +MODx.window.AddGroupToProfile = function(config = {}) { + Ext.applyIf(config, { + title: _('usergroup_create'), + fields: [{ + fieldLabel: _('user_group'), + name: 'usergroup', + hiddenName: 'usergroup', + id: 'modx-fcaug-usergroup', + xtype: 'modx-combo-usergroup', + editable: false, + allowBlank: false, + anchor: '100%' }] }); - MODx.window.AddGroupToProfile.superclass.constructor.call(this,config); + MODx.window.AddGroupToProfile.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.AddGroupToProfile,MODx.Window,{ +Ext.extend(MODx.window.AddGroupToProfile, MODx.Window, { submit: function() { - var rec = {}; - rec.id = Ext.getCmp('modx-fcaug-usergroup').getValue(); - rec.name = Ext.getCmp('modx-fcaug-usergroup').getRawValue(); + const + record = {}, + grid = Ext.getCmp('modx-grid-fc-profile-usergroups'), + store = grid.getStore(), + match = store.findExact('id', record.id) + ; + record.id = Ext.getCmp('modx-fcaug-usergroup').getValue(); + record.name = Ext.getCmp('modx-fcaug-usergroup').getRawValue(); - var g = Ext.getCmp('modx-grid-fc-profile-usergroups'); - var s = g.getStore(); - var v = s.findExact('id',rec.id); - if (v != '-1') { - MODx.msg.alert(_('error'),_('profile_usergroup_err_ae')); + if (match !== -1) { + MODx.msg.alert(_('error'), _('profile_usergroup_err_ae')); return false; } - this.fireEvent('success',rec); + this.fireEvent('success', record); this.hide(); return false; } }); -Ext.reg('modx-window-fc-profile-add-usergroup',MODx.window.AddGroupToProfile); +Ext.reg('modx-window-fc-profile-add-usergroup', MODx.window.AddGroupToProfile); From 2cf309e6724825ec7ad66f5998dad8ff53627d2f Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 7 Nov 2024 22:31:07 -0500 Subject: [PATCH 18/39] FC Profiles and Sets more additions Final functional and minor display changes --- core/lexicon/en/default.inc.php | 7 ++ core/lexicon/en/formcustomization.inc.php | 1 + core/lexicon/en/template.inc.php | 1 - .../assets/modext/widgets/core/modx.grid.js | 2 +- .../modext/widgets/fc/modx.grid.fcprofile.js | 31 ++++- .../modext/widgets/fc/modx.grid.fcset.js | 108 ++++++++++++++---- .../modext/widgets/fc/modx.panel.fcprofile.js | 1 + .../modext/widgets/fc/modx.panel.fcset.js | 2 + 8 files changed, 124 insertions(+), 29 deletions(-) diff --git a/core/lexicon/en/default.inc.php b/core/lexicon/en/default.inc.php index 8f2386dee4..212cd6629a 100644 --- a/core/lexicon/en/default.inc.php +++ b/core/lexicon/en/default.inc.php @@ -576,6 +576,13 @@ */ // All +// Templates (some entries also used in Form Customization) +$_lang['template_empty'] = '(empty)'; +$_lang['template_empty_desc'] = '(Note: A template has not been assigned to this set)'; +$_lang['template_missing'] = '(missing)'; +$_lang['template_missing_reassign'] = '(missing — please select a template or “empty” from this list)'; +$_lang['template_missing_desc'] = '(Note: The assigned template no longer exists)'; + // TVs $_lang['tv_type'] = 'Input Type'; $_lang['tv_default'] = 'Default Value'; diff --git a/core/lexicon/en/formcustomization.inc.php b/core/lexicon/en/formcustomization.inc.php index d9cc680cfa..56a977b339 100644 --- a/core/lexicon/en/formcustomization.inc.php +++ b/core/lexicon/en/formcustomization.inc.php @@ -80,6 +80,7 @@ $_lang['set_change_template_confirm'] = 'Are you sure you want to do this? This will change the Template that these Rules apply to. If so, MODX will first save your changes before reloading the page to refresh the new TVs for the new Template.'; $_lang['set_constraint_field_desc'] = 'Setting the Constraint field will prevent the rules in this Set from executing unless the field for this Resource matches the "constraint" value.'; $_lang['set_constraint_desc'] = 'Set the value of the field (specified above) to restrict the rules in this Set from being executed unless the Resource has this value on the specified constraint field.'; +$_lang['set_edit'] = 'Edit this set’s rules'; $_lang['set_err_nfs'] = 'No Set found with ID [[+id]]'; $_lang['set_err_ns'] = 'No Set specified.'; $_lang['set_fields_msg'] = 'Here you can adjust the fields for this page, including their visibility, labels and default values. Just double-click on a row to edit its value. Leave a field empty to use the default setting.
          Please note: when hiding an element inside this profile, it will be hidden in overlapping profiles too (even if Visible is checked).'; diff --git a/core/lexicon/en/template.inc.php b/core/lexicon/en/template.inc.php index 738b49ea20..eae311fade 100644 --- a/core/lexicon/en/template.inc.php +++ b/core/lexicon/en/template.inc.php @@ -18,7 +18,6 @@ $_lang['template_description_desc'] = 'Usage information for this Template shown in search results and as a tooltip in the Elements tree.'; $_lang['template_duplicate_confirm'] = 'Are you sure you want to duplicate this template?'; $_lang['template_edit_tab'] = 'Edit Template'; -$_lang['template_empty'] = '(empty)'; $_lang['template_err_default_template'] = 'This template is set as the default template. Please choose a different default template in the MODX configuration before deleting this template.
          '; $_lang['template_err_delete'] = 'An error occurred while trying to delete the template.'; $_lang['template_err_duplicate'] = 'An error occurred while duplicating the template.'; diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index de7bd1c0c0..59036e112b 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -1783,7 +1783,7 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { return rowClasses.length ? rowClasses.join(' ') : '' ; } // Add various classes marking a row as protected - if (hasBulkActions && !canDeleteRecord) { + if (hasBulkActions && !canDeleteRecord && !markActiveRows) { rowClasses.push('disable-selection'); } if (record.json.isProtected) { diff --git a/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js b/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js index 215cf390ce..1735579029 100644 --- a/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js +++ b/manager/assets/modext/widgets/fc/modx.grid.fcprofile.js @@ -74,11 +74,16 @@ MODx.grid.FCProfile = function(config = {}) { }, { header: _('name'), dataIndex: 'name', + id: 'modx-fc-profile--name', width: 200, sortable: true, - editor: { xtype: 'textfield' }, + editor: { + xtype: 'textarea' + }, renderer: { fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); return this.renderLink(value, { href: `?a=security/forms/profile/update&id=${record.data.id}` }); @@ -88,9 +93,20 @@ MODx.grid.FCProfile = function(config = {}) { }, { header: _('description'), dataIndex: 'description', + id: 'modx-fc-profile--description', width: 250, sortable: true, - editor: { xtype: 'textarea' } + editor: { + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return value; + }, + scope: this + } }, { header: _('usergroups'), dataIndex: 'usergroups', @@ -112,7 +128,7 @@ MODx.grid.FCProfile = function(config = {}) { }); MODx.grid.FCProfile.superclass.constructor.call(this, config); - this.gridMenuActions = ['edit', 'delete', 'duplicate']; + this.gridMenuActions = ['edit', 'delete', 'duplicate', 'activate']; // Note there are currently no action-specific, object-specific permissions for FC Profiles this.setUserCanEdit(['customize_forms', 'save']); @@ -120,7 +136,14 @@ MODx.grid.FCProfile = function(config = {}) { this.setUserCanDelete(['customize_forms', 'remove']); this.setShowActionsMenu(); - this.on('render', function() { this.getStore().reload(); }, this); + this.on({ + render: function() { + this.setEditableColumnAccess( + ['modx-fc-profile--name', 'modx-fc-profile--description'] + ); + this.getStore().reload(); + } + }); }; Ext.extend(MODx.grid.FCProfile, MODx.grid.Grid, { getMenu: function() { diff --git a/manager/assets/modext/widgets/fc/modx.grid.fcset.js b/manager/assets/modext/widgets/fc/modx.grid.fcset.js index b7fd6d01ca..dff679ffd2 100644 --- a/manager/assets/modext/widgets/fc/modx.grid.fcset.js +++ b/manager/assets/modext/widgets/fc/modx.grid.fcset.js @@ -1,5 +1,6 @@ MODx.grid.FCSet = function(config = {}) { this.sm = new Ext.grid.CheckboxSelectionModel(); + const actionCombo = new MODx.combo.FCAction(); Ext.applyIf(config, { id: 'modx-grid-fc-set', url: MODx.config.connector_url, @@ -32,55 +33,102 @@ MODx.grid.FCSet = function(config = {}) { dataIndex: 'id', width: 40, sortable: true + }, { + header: _('template'), + dataIndex: 'template', + id: 'modx-fc-set--template', + width: 150, + sortable: true, + renderer: { + fn: function(value, metaData, record) { + let + displayValue = record.json.templatename, + linkDescripton = _('set_edit') + ; + if (Ext.isEmpty(record.json.templatename)) { + if (record.json.template > 0) { + displayValue = _('template_missing'); + linkDescripton += `\n${_('template_missing_desc')}`; + } else { + displayValue = _('template_empty'); + linkDescripton += `\n${_('template_empty_desc')}`; + } + } + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return this.renderLink(displayValue, { + href: `?a=security/forms/set/update&id=${record.id}`, + title: linkDescripton + }); + }, + scope: this + } }, { header: _('action'), dataIndex: 'action', + id: 'modx-fc-set--action', width: 200, - editable: true, sortable: true, - editor: { - xtype: 'modx-combo-fc-action', - renderer: true + editor: actionCombo, + renderer: { + fn: function(value, metaData, record, rowIndex, colIndex) { + const actionRecord = actionCombo.findRecord(actionCombo.valueField, value); + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return actionRecord ? actionRecord.get(actionCombo.displayField) : value; + }, + scope: this } }, { header: _('description'), dataIndex: 'description', + id: 'modx-fc-set--description', width: 200, - editable: true, sortable: true, editor: { - xtype: 'textarea', - renderer: true - } - }, { - header: _('template'), - dataIndex: 'template', - width: 150, - sortable: true, - editable: true, - editor: { - xtype: 'modx-combo-template', - renderer: true + xtype: 'textarea' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return value; + }, + scope: this } }, { header: _('constraint_field'), dataIndex: 'constraint_field', + id: 'modx-fc-set--constraint_field', width: 200, - editable: true, sortable: false, editor: { - xtype: 'textfield', - renderer: true + xtype: 'textfield' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return value; + }, + scope: this } }, { header: _('constraint'), dataIndex: 'constraint', + id: 'modx-fc-set--constraint', width: 200, - editable: true, sortable: false, editor: { - xtype: 'textfield', - renderer: true + xtype: 'textfield' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return value; + }, + scope: this } }], tbar: [ @@ -111,6 +159,20 @@ MODx.grid.FCSet = function(config = {}) { this.setUserCanCreate(['customize_forms', 'save']); this.setUserCanDelete(['customize_forms', 'remove']); this.setShowActionsMenu(); + + this.on({ + render: function() { + this.setEditableColumnAccess( + [ + 'modx-fc-set--action', + 'modx-fc-set--description', + 'modx-fc-set--template', + 'modx-fc-set--constraint', + 'modx-fc-set--constraint_field' + ] + ); + } + }); }; Ext.extend(MODx.grid.FCSet, MODx.grid.Grid, { getMenu: function() { diff --git a/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js b/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js index cbef0c73e5..0a7f5099fc 100644 --- a/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js +++ b/manager/assets/modext/widgets/fc/modx.panel.fcprofile.js @@ -64,6 +64,7 @@ MODx.panel.FCProfile = function(config = {}) { }, { xtype: 'xcheckbox', fieldLabel: _('active'), + ctCls: 'display-switch', name: 'active', id: 'modx-fcp-active', inputValue: true, diff --git a/manager/assets/modext/widgets/fc/modx.panel.fcset.js b/manager/assets/modext/widgets/fc/modx.panel.fcset.js index 0503495b50..d2dfd980e6 100644 --- a/manager/assets/modext/widgets/fc/modx.panel.fcset.js +++ b/manager/assets/modext/widgets/fc/modx.panel.fcset.js @@ -61,6 +61,7 @@ MODx.panel.FCSet = function(config = {}) { name: 'template', hiddenName: 'template', value: config.record.template || 0, + valueNotFoundText: _('template_missing_reassign'), lazyInit: false, lazyRender: false, baseParams: { @@ -128,6 +129,7 @@ MODx.panel.FCSet = function(config = {}) { }, { xtype: 'xcheckbox', fieldLabel: _('active'), + ctCls: 'display-switch', name: 'active', inputValue: true, value: Boolean(config.record.active) From 5dd68308ec36653b2f6394742c3de8316add28f0 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 8 Nov 2024 00:32:05 -0500 Subject: [PATCH 19/39] Update modx.grid.user.js Formatting, code style changes only --- .../modext/widgets/security/modx.grid.user.js | 479 +++++++++--------- 1 file changed, 252 insertions(+), 227 deletions(-) diff --git a/manager/assets/modext/widgets/security/modx.grid.user.js b/manager/assets/modext/widgets/security/modx.grid.user.js index 5c7a7157b4..f19c1b071f 100644 --- a/manager/assets/modext/widgets/security/modx.grid.user.js +++ b/manager/assets/modext/widgets/security/modx.grid.user.js @@ -7,32 +7,32 @@ * @xtype modx-panel-users */ MODx.panel.Users = function(config = {}) { - Ext.applyIf(config,{ - id: 'modx-panel-users' - ,cls: 'container' - ,bodyStyle: '' - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('users') - ,id: 'modx-users-header' - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('users') - ,layout: 'form' - ,items: [{ - html: '

          '+_('user_management_msg')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-user' - ,cls:'main-wrapper' - ,preventRender: true + Ext.applyIf(config, { + id: 'modx-panel-users', + cls: 'container', + bodyStyle: '', + defaults: { collapsible: false, autoHeight: true }, + items: [{ + html: _('users'), + id: 'modx-users-header', + xtype: 'modx-header' + }, MODx.getPageStructure([{ + title: _('users'), + layout: 'form', + items: [{ + html: `

          ${_('user_management_msg')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-user', + cls: 'main-wrapper', + preventRender: true }] }])] }); - MODx.panel.Users.superclass.constructor.call(this,config); + MODx.panel.Users.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.Users,MODx.FormPanel); -Ext.reg('modx-panel-users',MODx.panel.Users); +Ext.extend(MODx.panel.Users, MODx.FormPanel); +Ext.reg('modx-panel-users', MODx.panel.Users); /** * Loads a grid of User. @@ -44,13 +44,13 @@ Ext.reg('modx-panel-users',MODx.panel.Users); */ MODx.grid.User = function(config = {}) { this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { - action: 'Security/User/GetList' - ,usergroup: MODx.request.usergroup || null - } - ,fields: [ + Ext.applyIf(config, { + url: MODx.config.connector_url, + baseParams: { + action: 'Security/User/GetList', + usergroup: MODx.request.usergroup || null + }, + fields: [ 'id', 'username', 'fullname', @@ -60,102 +60,113 @@ MODx.grid.User = function(config = {}) { 'role', 'active', 'cls' - ] - ,paging: true - ,autosave: true - ,save_action: 'Security/User/UpdateFromGrid' - ,autosaveErrorMsg: _('user_err_save') - ,remoteSort: true - ,viewConfig: { - forceFit:true - ,enableRowBody:true - ,scrollOffset: 0 - ,autoFill: true - ,showPreview: true - ,getRowClass : function(rec){ - return rec.data.active ? 'grid-row-active' : 'grid-row-inactive'; + ], + paging: true, + autosave: true, + save_action: 'Security/User/UpdateFromGrid', + autosaveErrorMsg: _('user_err_save'), + remoteSort: true, + viewConfig: { + forceFit: true, + enableRowBody: true, + scrollOffset: 0, + autoFill: true, + showPreview: true, + getRowClass: function(record) { + return record.data.active ? 'grid-row-active' : 'grid-row-inactive'; } - } - ,sm: this.sm - ,columns: [this.sm,{ - header: _('id') - ,dataIndex: 'id' - ,width: 50 - ,sortable: true - },{ - header: _('username') - ,dataIndex: 'username' - ,width: 150 - ,sortable: true - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/user/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('user_full_name') - ,dataIndex: 'fullname' - ,width: 180 - ,sortable: true - ,editor: { xtype: 'textfield' } - ,renderer: Ext.util.Format.htmlEncode - },{ - header: _('email') - ,dataIndex: 'email' - ,width: 180 - ,sortable: true - ,editor: { xtype: 'textfield' } - },{ - header: _('active') - ,dataIndex: 'active' - ,width: 80 - ,sortable: true - ,editor: { xtype: 'combo-boolean', renderer: 'boolean' } - },{ - header: _('user_block') - ,dataIndex: 'blocked' - ,width: 80 - ,sortable: true - ,editor: { xtype: 'combo-boolean', renderer: 'boolean' } - }] - ,tbar: [ + }, + sm: this.sm, + columns: [this.sm, { + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('username'), + dataIndex: 'username', + width: 150, + sortable: true, + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=security/user/update&id=${record.data.id}` + }); + }, + scope: this + } + }, { + header: _('user_full_name'), + dataIndex: 'fullname', + width: 180, + sortable: true, + editor: { xtype: 'textfield' }, + renderer: Ext.util.Format.htmlEncode + }, { + header: _('email'), + dataIndex: 'email', + width: 180, + sortable: true, + editor: { + xtype: 'textfield' + } + }, { + header: _('active'), + dataIndex: 'active', + width: 80, + sortable: true, + editor: { + xtype: 'combo-boolean', + renderer: 'boolean' + } + }, { + header: _('user_block'), + dataIndex: 'blocked', + width: 80, + sortable: true, + editor: { + xtype: 'combo-boolean', + renderer: 'boolean' + } + }], + tbar: [ { - text: _('create') - ,handler: this.createUser - ,scope: this - ,cls:'primary-button' - },{ - text: _('bulk_actions') - ,menu: [ + text: _('create'), + handler: this.createUser, + scope: this, + cls: 'primary-button' + }, { + text: _('bulk_actions'), + menu: [ { - text: _('selected_activate') - ,handler: this.activateSelected - ,scope: this - },{ - text: _('selected_deactivate') - ,handler: this.deactivateSelected - ,scope: this - },{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + text: _('selected_activate'), + handler: this.activateSelected, + scope: this + }, { + text: _('selected_deactivate'), + handler: this.deactivateSelected, + scope: this + }, { + text: _('selected_remove'), + handler: this.removeSelected, + scope: this } ] }, '->', { - xtype: 'modx-combo-usergroup' - ,itemId: 'filter-usergroup' - ,emptyText: `${_('user_group')}...` - ,baseParams: { - action: 'Security/Group/GetList' - ,addAll: true - } - ,value: MODx.request.usergroup || null - ,width: 200 - ,listeners: { + xtype: 'modx-combo-usergroup', + itemId: 'filter-usergroup', + emptyText: `${_('user_group')}...`, + baseParams: { + action: 'Security/Group/GetList', + addAll: true + }, + value: MODx.request.usergroup || null, + width: 200, + listeners: { select: { - fn: function (cmp, record, selectedIndex) { + fn: function(cmp, record, selectedIndex) { this.applyGridFilter(cmp, 'usergroup'); }, scope: this @@ -166,165 +177,179 @@ MODx.grid.User = function(config = {}) { this.getClearFiltersButton('filter-usergroup, filter-query') ] }); - MODx.grid.User.superclass.constructor.call(this,config); + MODx.grid.User.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.User,MODx.grid.Grid,{ +Ext.extend(MODx.grid.User, MODx.grid.Grid, { getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; + const + record = this.getSelectionModel().getSelected(), + menu = [], + p = record.data.cls + ; if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_activate') - ,handler: this.activateSelected - ,scope: this + menu.push({ + text: _('selected_activate'), + handler: this.activateSelected, + scope: this }); - m.push({ - text: _('selected_deactivate') - ,handler: this.deactivateSelected - ,scope: this + menu.push({ + text: _('selected_deactivate'), + handler: this.deactivateSelected, + scope: this }); - m.push('-'); - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + menu.push('-'); + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected, + scope: this }); } else { - if (p.indexOf('pupdate') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateUser + if (p.indexOf('pupdate') !== -1) { + menu.push({ + text: _('edit'), + handler: this.updateUser }); } - if (p.indexOf('pcopy') != -1) { - if (m.length > 0) m.push('-'); - m.push({ - text: _('duplicate') - ,handler: this.duplicateUser + if (p.indexOf('pcopy') !== -1) { + if (menu.length > 0) { menu.push('-'); } + menu.push({ + text: _('duplicate'), + handler: this.duplicateUser }); } - if (p.indexOf('premove') != -1) { - if (m.length > 0) m.push('-'); - m.push({ - text: _('delete') - ,handler: this.removeUser + if (p.indexOf('premove') !== -1) { + if (menu.length > 0) { menu.push('-'); } + menu.push({ + text: _('delete'), + handler: this.removeUser }); } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (menu.length > 0) { + this.addContextMenuItem(menu); } - } + }, - ,createUser: function() { + createUser: function() { MODx.loadPage('security/user/create'); - } + }, - ,updateUser: function() { - MODx.loadPage('security/user/update', 'id='+this.menu.record.id); - } + updateUser: function() { + MODx.loadPage('security/user/update', `id=${this.menu.record.id}`); + }, - ,duplicateUser: function() { + duplicateUser: function() { MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/User/Duplicate' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + url: this.config.url, + params: { + action: 'Security/User/Duplicate', + id: this.menu.record.id + }, + listeners: { + success: { fn: this.refresh, scope: this } } }); - } + }, - ,removeUser: function() { + removeUser: function() { MODx.msg.confirm({ - title: _('delete') - ,text: _('user_confirm_remove') - ,url: this.config.url - ,params: { - action: 'Security/User/Delete' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + title: _('delete'), + text: _('user_confirm_remove'), + url: this.config.url, + params: { + action: 'Security/User/Delete', + id: this.menu.record.id + }, + listeners: { + success: { fn: this.refresh, scope: this } } }); - } - - ,activateSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + activateSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/User/ActivateMultiple' - ,users: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'Security/User/ActivateMultiple', + users: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,deactivateSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + deactivateSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.Ajax.request({ - url: this.config.url - ,params: { - action: 'Security/User/DeactivateMultiple' - ,users: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + url: this.config.url, + params: { + action: 'Security/User/DeactivateMultiple', + users: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + removeSelected: function() { + const selections = this.getSelectedAsList(); + if (selections === false) { + return false; + } MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('user_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'Security/User/RemoveMultiple' - ,users: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + title: _('selected_remove'), + text: _('user_remove_multiple_confirm'), + url: this.config.url, + params: { + action: 'Security/User/RemoveMultiple', + users: selections + }, + listeners: { + success: { + fn: function() { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; - } + }, - ,rendGender: function(d,c) { - switch(d.toString()) { + rendGender: function(d, c) { + switch (d.toString()) { case '0': return '-'; case '1': return _('male'); case '2': return _('female'); + // no default } } }); -Ext.reg('modx-grid-user',MODx.grid.User); +Ext.reg('modx-grid-user', MODx.grid.User); From 8f4024f9bc9adcf6062511bc9d3b365a483c34be Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 8 Nov 2024 02:05:45 -0500 Subject: [PATCH 20/39] Users additions Updates display of and ability to select row actions (gear icon, bulk actions button). Also fixes index controller so users with view permissions can see the grid of Users. Lastly removes unused method for Users grid class. --- .../Processors/Security/User/GetList.php | 25 ++- .../assets/modext/widgets/core/modx.grid.js | 5 +- .../modext/widgets/security/modx.grid.user.js | 178 ++++++++++-------- .../default/security/user/index.class.php | 3 +- 4 files changed, 126 insertions(+), 85 deletions(-) diff --git a/core/src/Revolution/Processors/Security/User/GetList.php b/core/src/Revolution/Processors/Security/User/GetList.php index a138ff2fd8..a39946610a 100644 --- a/core/src/Revolution/Processors/Security/User/GetList.php +++ b/core/src/Revolution/Processors/Security/User/GetList.php @@ -1,4 +1,5 @@ getProperty('sort') === 'id') { $this->setProperty('sort', $this->modx->getAlias($this->classKey) . '.id'); } + + $this->canCreate = $this->modx->hasPermission('new_user') && $this->modx->hasPermission('save_user'); + $this->canEdit = $this->modx->hasPermission('edit_user') && $this->modx->hasPermission('save_user');; + $this->canRemove = $this->modx->hasPermission('delete_user'); + return $initialized; } @@ -115,11 +125,16 @@ public function prepareQueryAfterCount(xPDOQuery $c) */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); - $objectArray['blocked'] = $object->get('blocked') ? true : false; - $objectArray['cls'] = 'pupdate premove pcopy'; - unset($objectArray['password'], $objectArray['cachepwd'], $objectArray['salt']); + $userData = $object->toArray(); + $userData['blocked'] = $object->get('blocked') ? true : false; + $userData['permissions'] = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + unset($userData['password'], $userData['cachepwd'], $userData['salt']); - return $objectArray; + return $userData; } } diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index 59036e112b..ec64e5f795 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -1719,7 +1719,10 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { listeners: { render: { fn: function(btn) { - if (!this.userCanDelete && !hasMoreActions) { + if ( + (!this.userCanDelete && !hasMoreActions) + || (!this.userCanDelete && !this.userCanEdit && hasMoreActions) + ) { btn.hide(); } }, diff --git a/manager/assets/modext/widgets/security/modx.grid.user.js b/manager/assets/modext/widgets/security/modx.grid.user.js index f19c1b071f..e123fe5235 100644 --- a/manager/assets/modext/widgets/security/modx.grid.user.js +++ b/manager/assets/modext/widgets/security/modx.grid.user.js @@ -58,8 +58,7 @@ MODx.grid.User = function(config = {}) { 'gender', 'blocked', 'role', - 'active', - 'cls' + 'active' ], paging: true, autosave: true, @@ -89,70 +88,109 @@ MODx.grid.User = function(config = {}) { sortable: true, renderer: { fn: function(value, metaData, record) { - return this.renderLink(value, { - href: `?a=security/user/update&id=${record.data.id}` - }); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=security/user/update&id=${record.data.id}`, + title: _('user_edit_account') + }) + : value + ; }, scope: this } }, { header: _('user_full_name'), dataIndex: 'fullname', + id: 'modx-user--fullname', width: 180, sortable: true, editor: { xtype: 'textfield' }, - renderer: Ext.util.Format.htmlEncode + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return value; + }, + scope: this + } }, { header: _('email'), dataIndex: 'email', + id: 'modx-user--email', width: 180, sortable: true, editor: { xtype: 'textfield' + }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record); + return value; + }, + scope: this } }, { header: _('active'), dataIndex: 'active', + id: 'modx-user--active', width: 80, sortable: true, editor: { - xtype: 'combo-boolean', - renderer: 'boolean' + xtype: 'combo-boolean' + }, + renderer: { + fn: function(value, metaData, record) { + const + displayValue = this.rendYesNo(value, metaData), + classes = `${metaData.css} ${this.setEditableCellClasses(record)}` + ; + // eslint-disable-next-line no-param-reassign + metaData.css = classes; + return displayValue; + }, + scope: this } }, { header: _('user_block'), dataIndex: 'blocked', + id: 'modx-user--blocked', width: 80, sortable: true, editor: { - xtype: 'combo-boolean', - renderer: 'boolean' + xtype: 'combo-boolean' + }, + renderer: { + fn: function(value, metaData, record) { + const + displayValue = this.rendYesNo(value, metaData), + classes = `${metaData.css} ${this.setEditableCellClasses(record)}` + ; + // eslint-disable-next-line no-param-reassign + metaData.css = classes; + return displayValue; + }, + scope: this } }], tbar: [ { text: _('create'), + cls: 'primary-button', handler: this.createUser, scope: this, - cls: 'primary-button' - }, { - text: _('bulk_actions'), - menu: [ - { - text: _('selected_activate'), - handler: this.activateSelected, - scope: this - }, { - text: _('selected_deactivate'), - handler: this.deactivateSelected, - scope: this - }, { - text: _('selected_remove'), - handler: this.removeSelected, + listeners: { + render: { + fn: function(btn) { + if (!this.userCanCreate) { + btn.hide(); + } + }, scope: this } - ] + } }, + this.getBulkActionsButton('user', 'Security/User/RemoveMultiple', 'int', 'activate', 'deactivate'), '->', { xtype: 'modx-combo-usergroup', @@ -178,14 +216,35 @@ MODx.grid.User = function(config = {}) { ] }); MODx.grid.User.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete', 'duplicate', 'activate']; + + this.setUserCanEdit(['edit_user', 'save_user']); + this.setUserCanCreate(['new_user', 'save_user']); + this.setUserCanDelete(['delete_user']); + this.setShowActionsMenu(); + + this.on({ + render: function(grid) { + this.setEditableColumnAccess( + [ + 'modx-user--fullname', + 'modx-user--email', + 'modx-user--active', + 'modx-user--blocked' + ] + ); + }, + beforeedit: function(e) { + if (!this.userCanEditRecord(e.record)) { + return false; + } + } + }); }; Ext.extend(MODx.grid.User, MODx.grid.Grid, { getMenu: function() { - const - record = this.getSelectionModel().getSelected(), - menu = [], - p = record.data.cls - ; + const menu = []; if (this.getSelectionModel().getCount() > 1) { menu.push({ text: _('selected_activate'), @@ -197,27 +256,28 @@ Ext.extend(MODx.grid.User, MODx.grid.Grid, { handler: this.deactivateSelected, scope: this }); - menu.push('-'); - menu.push({ - text: _('selected_remove'), - handler: this.removeSelected, - scope: this - }); + if (this.userCanDelete) { + menu.push('-'); + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected + }); + } } else { - if (p.indexOf('pupdate') !== -1) { + if (this.userCanEdit) { menu.push({ text: _('edit'), handler: this.updateUser }); } - if (p.indexOf('pcopy') !== -1) { + if (this.userCanCreate) { if (menu.length > 0) { menu.push('-'); } menu.push({ text: _('duplicate'), handler: this.duplicateUser }); } - if (p.indexOf('premove') !== -1) { + if (this.userCanDelete) { if (menu.length > 0) { menu.push('-'); } menu.push({ text: _('delete'), @@ -312,44 +372,6 @@ Ext.extend(MODx.grid.User, MODx.grid.Grid, { } }); return true; - }, - - removeSelected: function() { - const selections = this.getSelectedAsList(); - if (selections === false) { - return false; - } - MODx.msg.confirm({ - title: _('selected_remove'), - text: _('user_remove_multiple_confirm'), - url: this.config.url, - params: { - action: 'Security/User/RemoveMultiple', - users: selections - }, - listeners: { - success: { - fn: function() { - this.getSelectionModel().clearSelections(true); - this.refresh(); - }, - scope: this - } - } - }); - return true; - }, - - rendGender: function(d, c) { - switch (d.toString()) { - case '0': - return '-'; - case '1': - return _('male'); - case '2': - return _('female'); - // no default - } } }); Ext.reg('modx-grid-user', MODx.grid.User); diff --git a/manager/controllers/default/security/user/index.class.php b/manager/controllers/default/security/user/index.class.php index ed940141a0..cbe0fd2497 100644 --- a/manager/controllers/default/security/user/index.class.php +++ b/manager/controllers/default/security/user/index.class.php @@ -1,4 +1,5 @@ modx->hasPermission('edit_user'); + return $this->modx->hasPermission('view_user'); } /** From 745a65ef60953c79fd7e1701d3b710f4d625b6bc Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 11 Nov 2024 11:51:02 -0500 Subject: [PATCH 21/39] Update modx.grid.dashboard.widgets.js Formatting, code styling changes only --- .../system/modx.grid.dashboard.widgets.js | 220 +++++++++--------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js b/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js index aaea2ab971..6695daad9f 100644 --- a/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js +++ b/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js @@ -11,14 +11,13 @@ MODx.grid.DashboardWidgets = function(config = {}) { '

          {description_trans}

          ' ) }); - this.sm = new Ext.grid.CheckboxSelectionModel(); - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + url: MODx.config.connector_url, + baseParams: { action: 'System/Dashboard/Widget/GetList' - } - ,fields: [ + }, + fields: [ 'id', 'name', 'name_trans', @@ -30,50 +29,53 @@ MODx.grid.DashboardWidgets = function(config = {}) { 'lexicon', 'size', 'cls' - ] - ,paging: true - ,remoteSort: true - ,sm: this.sm - ,plugins: [this.exp] - ,columns: [this.exp,this.sm,{ - header: _('id') - ,dataIndex: 'id' - ,width: 50 - ,sortable: true - },{ - header: _('name') - ,dataIndex: 'name_trans' - ,width: 150 - ,sortable: true - ,editable: false - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=system/dashboards/widget/update&id=' + record.data.id - }); - }, scope: this } - },{ - header: _('widget_type') - ,dataIndex: 'type' - ,width: 80 - ,sortable: true - },{ - header: _('widget_namespace') - ,dataIndex: 'namespace' - ,width: 120 - ,sortable: true - }] - ,tbar: [ + ], + paging: true, + remoteSort: true, + sm: this.sm, + plugins: [this.exp], + columns: [this.exp, this.sm, { + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('name'), + dataIndex: 'name_trans', + width: 150, + sortable: true, + editable: false, + renderer: { + fn: function(v, md, record) { + return this.renderLink(v, { + href: `?a=system/dashboards/widget/update&id=${record.data.id}` + }); + }, + scope: this + } + }, { + header: _('widget_type'), + dataIndex: 'type', + width: 80, + sortable: true + }, { + header: _('widget_namespace'), + dataIndex: 'namespace', + width: 120, + sortable: true + }], + tbar: [ { - text: _('create') - ,cls:'primary-button' - ,handler: this.createDashboard - ,scope: this - },{ - text: _('bulk_actions') - ,menu: [{ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + text: _('create'), + cls: 'primary-button', + handler: this.createDashboard, + scope: this + }, { + text: _('bulk_actions'), + menu: [{ + text: _('selected_remove'), + handler: this.removeSelected, + scope: this }] }, '->', @@ -81,83 +83,93 @@ MODx.grid.DashboardWidgets = function(config = {}) { this.getClearFiltersButton('filter-query-dashboardWidgets') ] }); - MODx.grid.DashboardWidgets.superclass.constructor.call(this,config); + MODx.grid.DashboardWidgets.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.DashboardWidgets,MODx.grid.Grid,{ +Ext.extend(MODx.grid.DashboardWidgets, MODx.grid.Grid, { getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; + const + r = this.getSelectionModel().getSelected(), + p = r.data.cls, + menu = [] + ; if (this.getSelectionModel().getCount() > 1) { - m.push({ - text: _('selected_remove') - ,handler: this.removeSelected - ,scope: this + menu.push({ + text: _('selected_remove'), + handler: this.removeSelected, + scope: this }); } else { - if (p.indexOf('pupdate') != -1) { - m.push({ - text: _('edit') - ,handler: this.updateWidget + if (p.indexOf('pupdate') !== -1) { + menu.push({ + text: _('edit'), + handler: this.updateWidget }); } - if (p.indexOf('premove') != -1) { - if (m.length > 0) m.push('-'); - m.push({ - text: _('delete') - ,handler: this.removeWidget + if (p.indexOf('premove') !== -1) { + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('delete'), + handler: this.removeWidget }); } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (menu.length > 0) { + this.addContextMenuItem(menu); } - } + }, - ,createDashboard: function() { + createDashboard: function() { MODx.loadPage('system/dashboards/widget/create'); - } + }, - ,updateWidget: function() { - MODx.loadPage('system/dashboards/widget/update', 'id='+this.menu.record.id); - } + updateWidget: function() { + MODx.loadPage('system/dashboards/widget/update', `id=${this.menu.record.id}`); + }, - ,removeWidget: function() { + removeWidget: function() { MODx.msg.confirm({ - title: _('delete') - ,text: _('widget_remove_confirm') - ,url: this.config.url - ,params: { - action: 'System/Dashboard/Widget/Remove' - ,id: this.menu.record.id - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + title: _('delete'), + text: _('widget_remove_confirm'), + url: this.config.url, + params: { + action: 'System/Dashboard/Widget/Remove', + id: this.menu.record.id + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); - } - - ,removeSelected: function() { - var cs = this.getSelectedAsList(); - if (cs === false) return false; + }, + removeSelected: function() { + const cs = this.getSelectedAsList(); + if (cs === false) { + return false; + } MODx.msg.confirm({ - title: _('selected_remove') - ,text: _('widget_remove_multiple_confirm') - ,url: this.config.url - ,params: { - action: 'System/Dashboard/Widget/RemoveMultiple' - ,widgets: cs - } - ,listeners: { - 'success': {fn:function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - },scope:this} + title: _('selected_remove'), + text: _('widget_remove_multiple_confirm'), + url: this.config.url, + params: { + action: 'System/Dashboard/Widget/RemoveMultiple', + widgets: cs + }, + listeners: { + success: { + fn: function(r) { + this.getSelectionModel().clearSelections(true); + this.refresh(); + }, + scope: this + } } }); return true; } }); -Ext.reg('modx-grid-dashboard-widgets',MODx.grid.DashboardWidgets); +Ext.reg('modx-grid-dashboard-widgets', MODx.grid.DashboardWidgets); From df04944707252c675371c221b8ac25a45c241d54 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Wed, 13 Nov 2024 11:50:17 -0500 Subject: [PATCH 22/39] Update modx.grid.access.context.js Formatting, code style changes only --- .../security/modx.grid.access.context.js | 360 +++++++++--------- 1 file changed, 190 insertions(+), 170 deletions(-) diff --git a/manager/assets/modext/widgets/security/modx.grid.access.context.js b/manager/assets/modext/widgets/security/modx.grid.access.context.js index 3410bc1564..4a681b8e6e 100644 --- a/manager/assets/modext/widgets/security/modx.grid.access.context.js +++ b/manager/assets/modext/widgets/security/modx.grid.access.context.js @@ -6,148 +6,171 @@ * @param {Object} config An object of options. * @xtype modx-grid-access-context */ -MODx.grid.AccessContext = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-grid-access-context' - ,url: MODx.config.connector_url - ,baseParams: { - action: 'Security/Access/GetList' - ,type: config.type || 'MODX\\Revolution\\modAccessContext' - ,target: config.context_key - } - ,fields: ['id','target','target_name','principal_class','principal','principal_name','authority','policy','policy_name','cls'] - ,type: 'modAccessContext' - ,paging: true - ,columns: [{ - header: _('context') - ,dataIndex: 'target_name' - ,width: 100 - },{ - header: _('user_group') - ,dataIndex: 'principal_name' - ,width: 120 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/usergroup/update&id=' + record.data.principal - ,target: '_blank' - }); - }, scope: this } - },{ - header: _('authority') - ,dataIndex: 'authority' - ,width: 50 - },{ - header: _('policy') - ,dataIndex: 'policy_name' - ,width: 175 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/access/policy/update&id=' + record.data.policy - ,target: '_blank' - }); - }, scope: this } - }] - ,tbar: [{ - text: _('create') - ,cls: 'primary-button' - ,scope: this - ,handler: this.createAcl +MODx.grid.AccessContext = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-grid-access-context', + url: MODx.config.connector_url, + baseParams: { + action: 'Security/Access/GetList', + type: config.type || 'MODX\\Revolution\\modAccessContext', + target: config.context_key + }, + fields: ['id', + 'target', + 'target_name', + 'principal_class', + 'principal', + 'principal_name', + 'authority', + 'policy', + 'policy_name', + 'cls' + ], + type: 'modAccessContext', + paging: true, + columns: [{ + header: _('context'), + dataIndex: 'target_name', + width: 100 + }, { + header: _('user_group'), + dataIndex: 'principal_name', + width: 120, + renderer: { + fn: function(value, metadata, record) { + return this.renderLink(value, { + href: `?a=security/usergroup/update&id=${record.data.principal}`, + target: '_blank' + }); + }, + scope: this + } + }, { + header: _('authority'), + dataIndex: 'authority', + width: 50 + }, { + header: _('policy'), + dataIndex: 'policy_name', + width: 175, + renderer: { + fn: function(value, metadata, record) { + return this.renderLink(value, { + href: `?a=security/access/policy/update&id=${record.data.policy}`, + target: '_blank' + }); + }, + scope: this + } + }], + tbar: [{ + text: _('create'), + cls: 'primary-button', + scope: this, + handler: this.createAcl }] }); - MODx.grid.AccessContext.superclass.constructor.call(this,config); + MODx.grid.AccessContext.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.AccessContext,MODx.grid.Grid,{ - combos: {} - ,windows: {} - - ,getMenu: function() { - var r = this.getSelectionModel().getSelected(); - var p = r.data.cls; - - var m = []; - if (this.getSelectionModel().getCount() > 1) { +Ext.extend(MODx.grid.AccessContext, MODx.grid.Grid, { + combos: {}, + windows: {}, - } else { - if (p.indexOf('pedit') != -1) { - m.push({ - text: _('edit') - ,handler: this.editAcl + getMenu: function() { + const + record = this.getSelectionModel().getSelected(), + p = record.data.cls, + menu = [] + ; + if (this.getSelectionModel().getCount() === 1) { + if (p.indexOf('pedit') !== -1) { + menu.push({ + text: _('edit'), + handler: this.editAcl }); } - if (p.indexOf('premove') != -1) { - if (m.length > 0) { m.push('-'); } - m.push({ - text: _('delete') - ,handler: this.removeAcl + if (p.indexOf('premove') !== -1) { + if (menu.length > 0) { menu.push('-'); } + menu.push({ + text: _('delete'), + handler: this.removeAcl }); } } - if (m.length > 0) { - this.addContextMenuItem(m); + if (menu.length > 0) { + this.addContextMenuItem(menu); } - } + }, - ,createAcl: function(itm,e) { - var r = { - target: this.config.context_key - ,principal_class: 'MODX\\Revolution\\modUserGroup' + createAcl: function(itm, e) { + const record = { + target: this.config.context_key, + principal_class: 'MODX\\Revolution\\modUserGroup' }; if (!this.windows.create_acl) { this.windows.create_acl = MODx.load({ - xtype: 'modx-window-access-context-create' - ,record: r - ,listeners: { - 'success': {fn:function(o) { - this.refresh(); - },scope:this} + xtype: 'modx-window-access-context-create', + record: record, + listeners: { + success: { + fn: function() { + this.refresh(); + }, + scope: this + } } }); } this.windows.create_acl.fp.getForm().reset(); - this.windows.create_acl.setValues(r); + this.windows.create_acl.setValues(record); this.windows.create_acl.show(e.target); - } + }, - ,editAcl: function(itm,e) { - var r = this.menu.record; - Ext.applyIf(r,{ - context: r.target - ,user_group: r.principal + editAcl: function(itm, e) { + const { record } = this.menu; + Ext.applyIf(record, { + context: record.target, + user_group: record.principal }); if (!this.windows.update_acl) { this.windows.update_acl = MODx.load({ - xtype: 'modx-window-access-context-update' - ,acl: r.id - ,record: r - ,listeners: { - 'success': {fn:this.refresh,scope:this} + xtype: 'modx-window-access-context-update', + acl: record.id, + record: record, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); } - this.windows.update_acl.setValues(r); + this.windows.update_acl.setValues(record); this.windows.update_acl.show(e.target); - } + }, - ,removeAcl: function(itm,e) { + removeAcl: function(itm, e) { MODx.msg.confirm({ - title: _('ugc_remove') - ,text: _('access_confirm_remove') - ,url: this.config.url - ,params: { - action: 'Security/Access/RemoveAcl' - ,id: this.menu.record.id - ,type: this.config.type || 'modAccessContext' - } - ,listeners: { - 'success': {fn:this.refresh,scope:this} + title: _('ugc_remove'), + text: _('access_confirm_remove'), + url: this.config.url, + params: { + action: 'Security/Access/RemoveAcl', + id: this.menu.record.id, + type: this.config.type || 'modAccessContext' + }, + listeners: { + success: { + fn: this.refresh, + scope: this + } } }); } }); -Ext.reg('modx-grid-access-context',MODx.grid.AccessContext); +Ext.reg('modx-grid-access-context', MODx.grid.AccessContext); /** * @class MODx.window.CreateAccessContext @@ -155,61 +178,59 @@ Ext.reg('modx-grid-access-context',MODx.grid.AccessContext); * @param {Object} config An object of options. * @xtype modx-window-access-context-create */ -MODx.window.CreateAccessContext = function(config) { - config = config || {}; - var r = config.record; - - Ext.applyIf(config,{ - title: _('ugc_mutate') - ,url: MODx.config.connector_url - ,baseParams: { - action: 'Security/Access/AddAcl' - ,type: config.type || 'modAccessContext' - } - ,type: 'modAccessContext' - ,acl: 0 - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - ,value: r.id || '' - },{ - xtype: 'hidden' - ,name: 'target' - ,value: r.context || '' - },{ - xtype: 'hidden' - ,name: 'principal_class' - ,value: 'MODX\\Revolution\\modUserGroup' - },{ - xtype: 'modx-combo-usergroup' - ,fieldLabel: _('user_group') - ,name: 'principal' - ,hiddenName: 'principal' - ,anchor: '100%' - ,value: r.principal || '' - ,baseParams: { - action: 'Security/Group/GetList' - ,combo: true +MODx.window.CreateAccessContext = function(config = {}) { + const { record } = config; + Ext.applyIf(config, { + title: _('ugc_mutate'), + url: MODx.config.connector_url, + baseParams: { + action: 'Security/Access/AddAcl', + type: config.type || 'modAccessContext' + }, + type: 'modAccessContext', + acl: 0, + fields: [{ + xtype: 'hidden', + name: 'id', + value: record.id || '' + }, { + xtype: 'hidden', + name: 'target', + value: record.context || '' + }, { + xtype: 'hidden', + name: 'principal_class', + value: 'MODX\\Revolution\\modUserGroup' + }, { + xtype: 'modx-combo-usergroup', + fieldLabel: _('user_group'), + name: 'principal', + hiddenName: 'principal', + anchor: '100%', + value: record.principal || '', + baseParams: { + action: 'Security/Group/GetList', + combo: true } - },{ - xtype: 'modx-combo-policy' - ,fieldLabel: _('policy') - ,name: 'policy' - ,hiddenName: 'policy' - ,value: r.policy || '' - ,anchor: '100%' - },{ - xtype: 'textfield' - ,fieldLabel: _('authority') - ,name: 'authority' - ,anchor: '100%' - ,value: r.authority || '' + }, { + xtype: 'modx-combo-policy', + fieldLabel: _('policy'), + name: 'policy', + hiddenName: 'policy', + value: record.policy || '', + anchor: '100%' + }, { + xtype: 'textfield', + fieldLabel: _('authority'), + name: 'authority', + anchor: '100%', + value: record.authority || '' }] }); - MODx.window.CreateAccessContext.superclass.constructor.call(this,config); + MODx.window.CreateAccessContext.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateAccessContext,MODx.Window); -Ext.reg('modx-window-access-context-create',MODx.window.CreateAccessContext); +Ext.extend(MODx.window.CreateAccessContext, MODx.Window); +Ext.reg('modx-window-access-context-create', MODx.window.CreateAccessContext); /** * @class MODx.window.UpdateAccessContext @@ -217,18 +238,17 @@ Ext.reg('modx-window-access-context-create',MODx.window.CreateAccessContext); * @param {Object} config An object of options. * @xtype modx-window-access-context-update */ -MODx.window.UpdateAccessContext = function(config) { - config = config || {}; - var r = config.record; - this.ident = config.ident || 'uactx'+Ext.id(); - Ext.applyIf(config,{ - title: _('ugc_mutate') - ,baseParams: { - action: 'Security/Access/UpdateAcl' - ,type: config.type || 'modAccessContext' +MODx.window.UpdateAccessContext = function(config = {}) { + // var r = config.record; + this.ident = config.ident || `uactx${Ext.id()}`; + Ext.applyIf(config, { + title: _('ugc_mutate'), + baseParams: { + action: 'Security/Access/UpdateAcl', + type: config.type || 'modAccessContext' } }); - MODx.window.UpdateAccessContext.superclass.constructor.call(this,config); + MODx.window.UpdateAccessContext.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.UpdateAccessContext,MODx.window.CreateAccessContext); -Ext.reg('modx-window-access-context-update',MODx.window.UpdateAccessContext); +Ext.extend(MODx.window.UpdateAccessContext, MODx.window.CreateAccessContext); +Ext.reg('modx-window-access-context-update', MODx.window.UpdateAccessContext); From 13482e7c213e8321d07bec38fb7547b971462521 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 14 Nov 2024 01:41:59 -0500 Subject: [PATCH 23/39] Relocate new Namespace method The new getExtrasNamespaces method was needed in places other than the main Namespaces page (via GetList); made static as well. Also updates GetList to use translatable Creator names. --- core/lexicon/en/default.inc.php | 3 +++ .../Workspace/PackageNamespace/GetList.php | 26 ++++--------------- core/src/Revolution/modNamespace.php | 19 ++++++++++++++ 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/core/lexicon/en/default.inc.php b/core/lexicon/en/default.inc.php index 212cd6629a..fddb96358f 100644 --- a/core/lexicon/en/default.inc.php +++ b/core/lexicon/en/default.inc.php @@ -319,6 +319,9 @@ $_lang['orm_container_rename'] = 'Rename Container'; $_lang['orm_container_remove'] = 'Delete Container'; $_lang['orm_container_remove_confirm'] = 'Are you sure you want to delete this container and all attributes below it? This is irreversible.'; +// "Extra(s)" below refers to a third-party software package. Translate to shortest length term possible. +$_lang['package_extra'] = 'Extra'; +$_lang['package_extras'] = 'Extras'; $_lang['pagetitle'] = 'Resource\'s Title'; $_lang['page_title'] = 'Resource Title'; $_lang['parameter'] = 'Parameter'; diff --git a/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php b/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php index 279318d377..a1f0dbe236 100644 --- a/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php +++ b/core/src/Revolution/Processors/Workspace/PackageNamespace/GetList.php @@ -74,7 +74,7 @@ public function initialize() $this->canEdit = $this->modx->hasPermission('namespaces'); $this->canRemove = $this->modx->hasPermission('namespaces'); $this->coreNamespaces = $this->classKey::getCoreNamespaces(); - $this->extrasNamespaces = $this->getExtrasNamespaces(); + $this->extrasNamespaces = modNamespace::class::getExtrasNamespaces($this->modx); return $initialized; } @@ -193,24 +193,6 @@ public function prepareQueryAfterCount(xPDOQuery $c) return $c; } - public function getExtrasNamespaces() - { - $namespaceList = []; - - $c = $this->modx->newQuery(modTransportPackage::class); - $c->select([ - 'name' => 'DISTINCT SUBSTRING_INDEX(`signature`,"-",1)' - ]); - $namespaces = $this->modx->getIterator(modTransportPackage::class, $c); - $namespaces->rewind(); - if ($namespaces->valid()) { - foreach ($namespaces as $namespace) { - $namespaceList[] = $namespace->get('name'); - } - } - return $namespaceList; - } - /** * Prepare the Namespace for listing * @param xPDOObject|modNamespace $object @@ -244,15 +226,17 @@ public function prepareRow(xPDOObject $object) switch (true) { case $namespaceData['isExtrasNamespace']: - $namespaceData['creator'] = 'extra'; + $namespaceData['creator'] = $this->modx->lexicon('package_extra'); break; case $isCoreNamespace: $namespaceData['creator'] = 'modx'; break; default: - $namespaceData['creator'] = 'user'; + $namespaceData['creator'] = $this->modx->lexicon('user'); $namespaceData['isProtected'] = false; } + $namespaceData['creator'] = strtolower($namespaceData['creator']); + // Core and Extras paths should only be editable via the installation process if ($isCoreNamespace || $namespaceData['isExtrasNamespace']) { $permissions = []; diff --git a/core/src/Revolution/modNamespace.php b/core/src/Revolution/modNamespace.php index d5d973d5fd..261da21f49 100644 --- a/core/src/Revolution/modNamespace.php +++ b/core/src/Revolution/modNamespace.php @@ -2,6 +2,7 @@ namespace MODX\Revolution; +use MODX\Revolution\Transport\modTransportPackage; use PDO; use xPDO\Cache\xPDOCacheManager; use xPDO\Om\xPDOCriteria; @@ -180,6 +181,24 @@ public function findPolicy($context = '') return $policy; } + public static function getExtrasNamespaces(modX $modx): array + { + $namespaceList = []; + + $c = $modx->newQuery(modTransportPackage::class); + $c->select([ + 'name' => 'DISTINCT SUBSTRING_INDEX(`signature`,"-",1)' + ]); + $namespaces = $modx->getIterator(modTransportPackage::class, $c); + $namespaces->rewind(); + if ($namespaces->valid()) { + foreach ($namespaces as $namespace) { + $namespaceList[] = $namespace->get('name'); + } + } + return $namespaceList; + } + /** * Returns a list of core Namespaces * From 7bdcb554944c1f3452605414b13c0f71ed1ba218 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 14 Nov 2024 21:45:31 -0500 Subject: [PATCH 24/39] Update modx.grid.js Consolidate shared methods and config elements into new GridBase class --- .../assets/modext/widgets/core/modx.grid.js | 3051 ++++++++--------- 1 file changed, 1351 insertions(+), 1700 deletions(-) diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index ec64e5f795..37cb696704 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -1,44 +1,16 @@ Ext.namespace('MODx.grid'); -MODx.grid.Grid = function(config = {}) { +/** + * @class MODx.grid.GridBase + * @extends Ext.grid.EditorGridPanel + * @param {Object} config An object of configuration properties + */ +MODx.grid.GridBase = function GridBase(config = {}) { this.config = config; this._loadStore(); this._loadColumnModel(); + this._loadMenu(); - Ext.applyIf(config, { - store: this.store, - cm: this.cm, - sm: new Ext.grid.RowSelectionModel({ singleSelect: true }), - // eslint-disable-next-line no-unneeded-ternary - paging: config.bbar ? true : false, - loadMask: true, - autoHeight: true, - collapsible: true, - stripeRows: true, - header: false, - cls: 'modx-grid', - preventRender: true, - preventSaveRefresh: true, - showPerPage: true, - stateful: false, - showActionsColumn: true, - disableContextMenuAction: false, - menuConfig: { - defaultAlign: 'tl-b?', - enableScrolling: false - }, - viewConfig: { - forceFit: true, - enableRowBody: true, - autoFill: true, - showPreview: true, - scrollOffset: 0, - emptyText: config.emptyText || _('ext_emptymsg') - }, - groupingConfig: { - enableGroupingMenu: true - } - }); if (config.paging) { const pgItms = config.showPerPage ? [`${_('per_page')}:`, { xtype: 'textfield', @@ -97,6 +69,33 @@ MODx.grid.Grid = function(config = {}) { } } + Ext.applyIf(config, { + store: this.store, + sm: new Ext.grid.RowSelectionModel({ + singleSelect: false + }), + cls: 'modx-grid', + collapsible: true, + disableContextMenuAction: false, + header: false, + loadMask: true, + menuConfig: { + defaultAlign: 'tl-b?', + enableScrolling: false + }, + paging: Boolean(config.bbar), + showActionsColumn: true, + stripeRows: true, + viewConfig: { + forceFit: true, + enableRowBody: true, + autoFill: true, + showPreview: true, + scrollOffset: 0, + emptyText: config.emptyText || _('ext_emptymsg') + } + }); + if (config.showActionsColumn) { let defaultActionsColumnWidth = 50; @@ -106,7 +105,6 @@ MODx.grid.Grid = function(config = {}) { return false; } } - return true; }; @@ -141,23 +139,13 @@ MODx.grid.Grid = function(config = {}) { } } - MODx.grid.Grid.superclass.constructor.call(this, config); - this._loadMenu(config); - this.addEvents('beforeRemoveRow', 'afterRemoveRow', 'afterAutoSave'); - if (this.autosave) { - this.on('afterAutoSave', this.onAfterAutoSave, this); - } - if (!config.preventRender) { - this.render(); - } + MODx.grid.GridBase.superclass.constructor.call(this, config); + + this.addEvents('beforeRemoveRow', 'afterRemoveRow'); + this.on({ - render: { - fn: function() { - const topToolbar = this.getTopToolbar(); - if (topToolbar && topToolbar.initialConfig.cls && topToolbar.initialConfig.cls === 'has-nested-filters') { - this.hasNestedFilters = true; - } - }, + click: { + fn: this.onClickHandler, scope: this }, rowcontextmenu: { @@ -165,41 +153,14 @@ MODx.grid.Grid = function(config = {}) { scope: this } }); - if (config.autosave) { - this.on('afteredit', this.saveRecord, this); - } - - if (config.paging && config.grouping) { - this.getBottomToolbar().bind(this.store); - } - - if (!config.paging && !Object.hasOwn(config, 'pageSize')) { - config.pageSize = 0; - } - this.getStore().load({ - params: { - start: config.pageStart || 0, - limit: Object.hasOwn(config, 'pageSize') ? config.pageSize : (parseInt(MODx.config.default_per_page, 10) || 20) - } - }); - this.getStore().on('exception', this.onStoreException, this); - this.config = config; - - this.on('click', this.onClickHandler, this); }; -Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { +Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { + currentLanguage: MODx.config.cultureKey || 'en', windows: {}, protectedIdentifiers: null, - /** - * The data index, not necessarily the primary key, used - * to determine if a row can be deleted / or if the value - * of the row's data index is an un-usable, reserved value - */ - protectedDataIndex: null, - userCanEdit: false, userCanCreate: false, @@ -208,6 +169,13 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { gridMenuActions: [], + /** + * @property {Boolean} hasNestedFilters - Indicates whether the top toolbar filter(s) are nested + * within a secondary container; they will be nested when they have labels and those labels are + * positioned above the filter's input. + */ + hasNestedFilters: false, + /** @property {Boolean} userHasPermissions Whether user has permissions of any kind to manipulate the current grid's data */ hasPermissions: false, @@ -216,421 +184,403 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { showActionsMenu: null, - onStoreException: function(dataProxy, type, action, options, response) { - const responseStatusCode = response.status || 'Unknown', - responseStatusText = !Ext.isEmpty(response.statusText) ? `(${response.statusText})` : '' - ; - let output = '', - msg = '' - ; - if (Ext.isEmpty(response.responseText)) { - // When php display_error is off, responseText will likely be empty and only general status info will be available - output = responseStatusCode !== 200 ? `
          ${responseStatusCode} ${responseStatusText}
          ` : ''; - } else { - // When php display_error is on OR the error is caught and explicity sent from the MODx class triggering the error, responseText should contain error text or possibly an object containing message text - try { - const responseText = Ext.decode(response.responseText); - // In what scenario will responseText be an object with a message property? - if (responseText && responseText.message) { - output = responseText.message; + // -*-*-* LOADERS *-*-*- + + _loadColumnModel: function() { + if (this.config.columns) { + const c = this.config.columns; + for (let i = 0; i < c.length; i++) { + // if specifying custom editor/renderer + if (typeof (c[i].editor) == 'string') { + // eslint-disable-next-line no-eval + c[i].editor = eval(c[i].editor); + } + if (typeof (c[i].renderer) == 'string') { + // eslint-disable-next-line no-eval + c[i].renderer = eval(c[i].renderer); + } + if (typeof (c[i].editor) == 'object' && c[i].editor.xtype) { + const r = c[i].editor.renderer; + // if (Ext.isEmpty(c[i].editor.id)) { c[i].editor.id = Ext.id(); } + c[i].editor = Ext.ComponentMgr.create(c[i].editor); + if (r === true) { + if (c[i].editor && c[i].editor.store && !c[i].editor.store.isLoaded && c[i].editor.config.mode !== 'local') { + c[i].editor.store.load(); + c[i].editor.store.isLoaded = true; + } + c[i].renderer = Ext.util.Format.comboRenderer(c[i].editor); + } else if (c[i].editor.initialConfig.xtype === 'datefield') { + c[i].renderer = Ext.util.Format.dateRenderer(c[i].editor.initialConfig.format || 'Y-m-d'); + } else if (r === 'boolean') { + c[i].renderer = this.rendYesNo; + } else if (r === 'password') { + c[i].renderer = this.rendPassword; + } else if (r === 'local' && typeof (c[i].renderer) == 'string') { + // eslint-disable-next-line no-eval + c[i].renderer = eval(c[i].renderer); + } + } + + /** + * When no renderer is provided, automatically apply the htmlEncode renderer to protect + * against XSS vulnerabilities. Columns that do have a renderer applied are assumed to + * implement their own protection. + */ + if (Ext.isEmpty(c[i].renderer)) { + c[i].renderer = Ext.util.Format.htmlEncode; + } + + /** + * When the field has an editor defined, wrap the (optional) renderer with + * a special renderer that applies a class and tooltip to indicate the + * column is editable. + */ + if (c[i].editor) { + c[i].renderer = this.renderEditableColumn(c[i].renderer); } - } catch (e) { - output = response.responseText; } + this.cm = new Ext.grid.ColumnModel(c); } - if (output) { - if (MODx.config.debug > 0) { - output = MODx.util.safeHtml(output, '
          ', 'class,colspan,rowspan'); - msg = _('error_grid_get_content_toscreen', { - message: `
          ${output}
          ` - }); - } else { - msg = _('error_grid_get_content_tolog'); - output = Ext.util.Format.stripTags(output).replaceAll('>', '>').replaceAll('<', '<'); - console.error(output); + }, + + _loadMenu: function() { + this.menu = new Ext.menu.Menu(this.config.menuConfig); + }, + + loadWindow: function(btn, e, win, or) { + const r = this.menu.record; + if (!this.windows[win.xtype] || win.force) { + Ext.applyIf(win, { + record: win.blankValues ? {} : r, + grid: this, + listeners: { + success: { + fn: win.success || this.refresh, + scope: win.scope || this + } + } + }); + if (or) { + Ext.apply(win, or); } - } else { - // With some scenarios, such as when php display_errors = 1 and MODx system setting debug = 0 (reporting off), the reponseText will be empty and the status will be 200 - msg = _('error_grid_get_content_no_msg'); + this.windows[win.xtype] = Ext.ComponentMgr.create(win); } - this.getView().emptyText = `
          ${msg}
          `; - this.getView().refresh(false); + if (this.windows[win.xtype].setValues && win.blankValues !== true && r !== undefined) { + this.windows[win.xtype].setValues(r); + } + this.windows[win.xtype].show(e.target); }, + // -*-*-* ROW EXPANDER *-*-*- + /** - * Executes auto save of the row after edits are complete and optional success callback - * @param {Ext.Event} e Extended event data including: - * * column - * * row - * * field (name) - * * grid (full grid object) - * * record (full Ext record object including store, data, json, etc.) - * * originalValue - * * value (current) + * Returns first found expander plugin + * @param plugins */ - saveRecord: function(e) { - e.record.data.menu = null; - const p = this.config.saveParams || {}; - Ext.apply(e.record.data, p); - const - data = Ext.util.JSON.encode(e.record.data), - url = this.config.saveUrl || (this.config.url || this.config.connector) - ; - MODx.Ajax.request({ - url: url, - params: { - action: this.config.save_action || 'updateFromGrid', - data: data - }, - listeners: { - success: { - fn: function(response) { - if (this.config.save_callback) { - Ext.callback(this.config.save_callback, this.config.scope || this, [response]); - } - e.record.commit(); - if (!this.config.preventSaveRefresh) { - const gridRefresh = new Ext.util.DelayedTask(() => this.refresh()); - gridRefresh.delay(200); - } - const - /** @var {Object} eventData Plucking only the needed event props to forward in the post-save event */ - eventData = { field: e.field, originalValue: e.originalValue, value: e.value }, - responseData = { ...response, eventData } - ; - this.fireEvent('afterAutoSave', responseData); - }, - scope: this - }, - failure: { - fn: function(response) { - e.record.reject(); - this.fireEvent('afterAutoSave', response); - }, - scope: this - } + findExpanderPlugin: function(plugins) { + if (Ext.isObject(plugins)) { + plugins = [plugins]; + } + + const index = Ext.each(plugins, function(item) { + if (item.id !== undefined && item.id === 'expander') { + return false; } }); + + return plugins[index]; }, - /** - * Method executed after a record has been edited/saved inline from within the grid - * - * @param {Object} response - The processor save response object. See modConnectorResponse::outputContent (PHP) - */ - onAfterAutoSave: function(response) { - if (!response.success && response.message === '') { - let msg = ''; - if (response.data.length) { - // We get some data for specific field(s) error but not regular error message - Ext.each(response.data, function(data, index, list) { - msg += (msg !== '' ? '
          ' : '') + data.msg; - }, this); - } - if (Ext.isEmpty(msg)) { - // Still no valid message so far, let's use some fallback - msg = this.autosaveErrorMsg || _('error'); - } - MODx.msg.alert(_('error'), msg); + expandAll: function() { + const expander = this.findExpanderPlugin(this.config.plugins); + + if (!expander) { + return false; } - }, - onChangePerPage: function(tf, nv) { - if (Ext.isEmpty(nv)) { return false; } - nv = parseInt(nv, 10); - this.getBottomToolbar().pageSize = nv; - this.store.load({ - params: { - start: 0, - limit: nv - } - }); + const rows = this.getView().getRows(); + + for (let i = 0; i < rows.length; i++) { + expander.expandRow(rows[i]); + } + + if (this.tools.plus !== undefined) { + this.tools.plus.hide(); + } + + if (this.tools.minus !== undefined) { + this.tools.minus.show(); + } + + return true; }, - loadWindow: function(btn, e, win, or) { - const r = this.menu.record; - if (!this.windows[win.xtype] || win.force) { - Ext.applyIf(win, { - record: win.blankValues ? {} : r, - grid: this, - listeners: { - success: { - fn: win.success || this.refresh, - scope: win.scope || this - } - } - }); - if (or) { - Ext.apply(win, or); - } - this.windows[win.xtype] = Ext.ComponentMgr.create(win); + collapseAll: function() { + const expander = this.findExpanderPlugin(this.config.plugins); + + if (!expander) { + return false; } - if (this.windows[win.xtype].setValues && win.blankValues !== true && r !== undefined) { - this.windows[win.xtype].setValues(r); + + const rows = this.getView().getRows(); + + for (let i = 0; i < rows.length; i++) { + expander.collapseRow(rows[i]); } - this.windows[win.xtype].show(e.target); + + if (this.tools.minus !== undefined) { + this.tools.minus.hide(); + } + + if (this.tools.plus !== undefined) { + this.tools.plus.show(); + } + + return true; }, - confirm: function(type, text) { - const - p = { action: type }, - k = this.config.primaryKey || 'id' - ; - p[k] = this.menu.record[k]; + // -*-*-* ROW ACTIONS & MENUS *-*-*- - MODx.msg.confirm({ - title: _(type), - text: _(text) || _('confirm_remove'), - url: this.config.url, - params: p, - listeners: { - success: { fn: this.refresh, scope: this } - } + _getActionsColumnTpl: function() { + return new Ext.XTemplate('' + + '' + + '
            ' + + '' + + '
          • ' + + '
            ' + + '
          ' + + '
          ' + + '
          ', { + compiled: true }); }, - remove: function(text, action) { - if (this.destroying) { - return MODx.grid.Grid.superclass.remove.apply(this, arguments); + actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { + /* + Note: To maintain backward compatibility for core grids that have not yet been updated + to the new permissions checks and for extras that may extend this class in their grids, + we check showActionsMenu for strict boolean values (which will only be set by grids using + the new checks); otherwise showActionsMenu will be null (its default value set above), + indicating the legacy checks are to be used. + */ + if (this.showActionsMenu === false) { + return; } - const r = this.menu.record; - text = text || 'confirm_remove'; - const p = this.config.saveParams || {}; - Ext.apply(p, { action: action || 'remove' }); - const k = this.config.primaryKey || 'id'; - p[k] = r[k]; - - if (this.fireEvent('beforeRemoveRow', r)) { - MODx.msg.confirm({ - title: _('warning'), - text: _(text, r), - url: this.config.url, - params: p, - listeners: { - success: { - fn: function() { - this.removeActiveRow(r); - }, - scope: this + /* + showActionsMenu will be true if at least one user group-level permission is granted, + excluding create/new permissions (since that is not executed by our context/actions menus). + */ + if (this.showActionsMenu) { + const { isProtected } = record.json; + // Export is always available; only continue filtering if grid does not offer export + if (!this.gridMenuActions.includes('export')) { + if (!this.userHasSavePermissions && isProtected) { + return; + } + // Checking record-level permissions; this block checking for 'cls' can be removed once all grids are updated + if (Object.hasOwn(record.data, 'cls')) { + if (Ext.isEmpty(record.data.cls)) { + return; + } + } + if (Object.hasOwn(record.json, 'permissions')) { + if ( + Ext.isEmpty(record.json.permissions) + || Object.values(record.json.permissions).every(permission => !permission) + ) { + return; } } + } + } + // eslint-disable-next-line prefer-spread + const actions = this.getActions.apply(this, arguments); + + if (this.config.disableContextMenuAction !== true) { + actions.push({ + text: _('context_menu'), + action: 'contextMenu', + icon: 'gear' }); } + return this._getActionsColumnTpl().apply({ + actions: actions + }); }, - removeActiveRow: function(r) { - if (this.fireEvent('afterRemoveRow', r)) { - const rx = this.getSelectionModel().getSelected(); - this.getStore().remove(rx); - } + getActions: function(value, metaData, record, rowIndex, colIndex, store) { + return []; }, - _loadMenu: function() { - this.menu = new Ext.menu.Menu(this.config.menuConfig); + actionContextMenu: function(record, recordIndex, e) { + this._showMenu(this, recordIndex, e); }, - _showMenu: function(g, ri, e) { - e.stopEvent(); - e.preventDefault(); - this.menu.record = this.getStore().getAt(ri).data; - if (!this.getSelectionModel().isSelected(ri)) { - this.getSelectionModel().selectRow(ri); - } - this.menu.removeAll(); - let menu; - if (this.getMenu) { - menu = this.getMenu(g, ri, e); - if (menu && menu.length && menu.length > 0) { - this.addContextMenuItem(menu); + addContextMenuItem: function(items) { + items.forEach(menuItem => { + if (menuItem === '-') { + this.menu.add('-'); + } else { + let handler = Ext.emptyFn; + if (menuItem.handler) { + // eslint-disable-next-line no-eval + handler = eval(menuItem.handler); + if (handler && typeof (handler) == 'object' && handler.xtype) { + handler = this.loadWindow.createDelegate(this, [handler], true); + } + } else { + handler = function(item) { + const + { options } = item.options, + { id } = this.menu.record, + doAction = (id, options) => { + const + action = Ext.urlEncode(options.params || { action: options.action }), + query = `?id=${id}&${action}`, + content = Ext.get('modx_content') + ; + if (content === null) { + window.location.href = query; + } else { + content.dom.src = query; + } + } + ; + if (options.confirm) { + // eslint-disable-next-line prefer-arrow-callback, func-names + Ext.Msg.confirm('', options.confirm, function(e) { + if (e === 'yes') { + doAction(id, options); + } + }, this); + } else { + doAction(id, options); + } + }; + } + this.menu.add({ + id: menuItem.id || Ext.id(), + text: menuItem.text, + scope: menuItem.scope || this, + options: menuItem, + handler: handler + }); } - } - if ((!menu || menu.length <= 0) && this.menu.record.menu) { - this.addContextMenuItem(this.menu.record.menu); - } - if (this.menu.items.length > 0) { - this.menu.showAt(e.xy); - } + }); }, - _loadStore: function() { - if (this.config.grouping) { - this.store = new Ext.data.GroupingStore({ - url: this.config.url, - baseParams: this.config.baseParams || { action: this.config.action || 'getList' }, - reader: new Ext.data.JsonReader({ - totalProperty: 'total', - root: 'results', - fields: this.config.fields - }), - sortInfo: { - field: this.config.sortBy || 'id', - direction: this.config.sortDir || 'ASC' - }, - remoteSort: this.config.remoteSort || false, - remoteGroup: this.config.remoteGroup || false, - groupField: this.config.groupBy || 'name', - groupDir: this.config.groupDir || 'ASC', - storeId: this.config.storeId || Ext.id(), - autoDestroy: true, - listeners: { - beforeload: function(store, options) { - const changedGroupDir = store.groupField === store.sortInfo.field && store.groupDir !== store.sortInfo.direction; - if (changedGroupDir) { - store.groupDir = store.sortInfo.direction; - store.baseParams.groupDir = store.sortInfo.direction; - } - }, - load: function(store, records, options) { - const cmp = Ext.getCmp('modx-content'); - if (cmp) { - cmp.doLayout(); - } - }, - groupchange: { - fn: function(store, groupField) { - store.groupDir = this.config.groupDir || 'ASC'; - store.baseParams.groupDir = store.groupDir; - store.sortInfo.direction = this.config.sortDir || 'ASC'; - store.load(); - }, - scope: this - } - } - }); - } else { - this.store = new Ext.data.JsonStore({ - url: this.config.url, - baseParams: this.config.baseParams || { action: this.config.action || 'getList' }, - fields: this.config.fields, - root: 'results', - totalProperty: 'total', - remoteSort: this.config.remoteSort || false, - storeId: this.config.storeId || Ext.id(), - autoDestroy: true, - listeners: { - load: function() { - const cmp = Ext.getCmp('modx-content'); - if (cmp) { - cmp.doLayout(); - } - } - } - }); + /** + * @property {Function} setShowActionsMenu - Based on properties set in the calling child class and the + * the current user's permissions for actions taken within that class (create, edit, delete, etc), + * evaluates whether the actions menu trigger should appear and sets boolean value on the showActionsMenu property + * + * @return void + */ + setShowActionsMenu: function() { + if (this.config.disableContextMenuAction === true) { + this.showActionsMenu = false; + return; } + const permissionsValues = []; + this.gridMenuActions.forEach(mode => { + mode = mode === 'duplicate' ? 'userCanCreate' : `userCan${Ext.util.Format.capitalize(mode)}`; + const modePermission = mode === 'userCanExport' ? true : this[mode]; + if (['userCanCreate', 'userCanEdit'].includes(mode) && modePermission === true) { + this.userHasSavePermissions = true; + } + permissionsValues.push(modePermission); + }); + this.showActionsMenu = !(permissionsValues.length === 0 || permissionsValues.every(value => value === false) === true); }, - _loadColumnModel: function() { - if (this.config.columns) { - const c = this.config.columns; - for (let i = 0; i < c.length; i++) { - // if specifying custom editor/renderer - if (typeof (c[i].editor) == 'string') { - // eslint-disable-next-line no-eval - c[i].editor = eval(c[i].editor); - } - if (typeof (c[i].renderer) == 'string') { - // eslint-disable-next-line no-eval - c[i].renderer = eval(c[i].renderer); - } - if (typeof (c[i].editor) == 'object' && c[i].editor.xtype) { - const r = c[i].editor.renderer; - if (Ext.isEmpty(c[i].editor.id)) { c[i].editor.id = Ext.id(); } - c[i].editor = Ext.ComponentMgr.create(c[i].editor); - if (r === true) { - if (c[i].editor && c[i].editor.store && !c[i].editor.store.isLoaded && c[i].editor.config.mode !== 'local') { - c[i].editor.store.load(); - c[i].editor.store.isLoaded = true; - } - c[i].renderer = Ext.util.Format.comboRenderer(c[i].editor); - } else if (c[i].editor.initialConfig.xtype === 'datefield') { - c[i].renderer = Ext.util.Format.dateRenderer(c[i].editor.initialConfig.format || 'Y-m-d'); - } else if (r === 'boolean') { - c[i].renderer = this.rendYesNo; - } else if (r === 'password') { - c[i].renderer = this.rendPassword; - } else if (r === 'local' && typeof (c[i].renderer) == 'string') { - // eslint-disable-next-line no-eval - c[i].renderer = eval(c[i].renderer); - } - } - - /** - * When no renderer is provided, automatically apply the htmlEncode renderer to protect - * against XSS vulnerabilities. Columns that do have a renderer applied are assumed to - * implement their own protection. - */ - if (Ext.isEmpty(c[i].renderer)) { - c[i].renderer = Ext.util.Format.htmlEncode; - } - - /** - * When the field has an editor defined, wrap the (optional) renderer with - * a special renderer that applies a class and tooltip to indicate the - * column is editable. - */ - if (c[i].editor) { - c[i].renderer = this.renderEditableColumn(c[i].renderer); - } + _showMenu: function(g, ri, e) { + e.stopEvent(); + e.preventDefault(); + this.menu.record = this.getStore().getAt(ri).data; + if (!this.getSelectionModel().isSelected(ri)) { + this.getSelectionModel().selectRow(ri); + } + this.menu.removeAll(); + let menu; + if (this.getMenu) { + menu = this.getMenu(g, ri, e); + if (menu && menu.length && menu.length > 0) { + this.addContextMenuItem(menu); } - this.cm = new Ext.grid.ColumnModel(c); + } + if ((!menu || menu.length <= 0) && this.menu.record.menu) { + this.addContextMenuItem(this.menu.record.menu); + } + if (this.menu.items.length > 0) { + this.menu.showAt(e.xy); } }, - addContextMenuItem: function(items) { - const l = items.length; - for (let i = 0; i < l; i++) { - const options = items[i]; + // -*-*-* PERMISSIONS HANDLING *-*-*- - if (options === '-') { - this.menu.add('-'); - continue; - } - let h = Ext.emptyFn; - if (options.handler) { - // eslint-disable-next-line no-eval - h = eval(options.handler); - if (h && typeof (h) == 'object' && h.xtype) { - h = this.loadWindow.createDelegate(this, [h], true); + /** + * Deprecated; renamed checkCellIsEditable. Remove in 3.1 + */ + checkEditable: function(e) { + this.checkCellIsEditable(e); + }, + + /** + * Disables cell editor under specified conditions + * @param {Object} e - Ext event object containing references to grid, record, field, value, row (index), column (index), and cancel (set true to cancel edit). + * @return {Boolean} Return false to cancel or true to commit the edit + */ + checkCellIsEditable: function(e) { + const permissions = e.record.data.perm || ''; + if (permissions.indexOf('edit') === -1) { + return false; + } + // Grid-specific conditions + switch (e.grid.xtype) { + case 'modx-grid-role': { + const + isAuthorityField = e.field === 'authority', + roleIsAssigned = e.record.json.isAssigned + ; + if (roleIsAssigned && isAuthorityField) { + return false; } - } else { - h = function(itm) { - const - o = itm.options, - { id } = this.menu.record - ; - if (o.confirm) { - Ext.Msg.confirm('', o.confirm, function(e) { - if (e === 'yes') { - const act = Ext.urlEncode(o.params || { action: o.action }); - window.location.href = `?id=${id}&${act}`; - } - }, this); - } else { - const act = Ext.urlEncode(o.params || { action: o.action }); - window.location.href = `?id=${id}&${act}`; - } - }; + break; } - this.menu.add({ - id: options.id || Ext.id(), - text: options.text, - scope: options.scope || this, - options: options, - handler: h - }); + default: } + return true; }, - refresh: function() { - this.getStore().reload(); - }, - - rendPassword: function(v) { - let z = ''; - for (let i = 0; i < v.length; i++) { - z = `${z}*`; + /** + * Add one or more classes to a specific Editor Grid cell, typically to indicate a level of restriction + * + * @param {Object} record - The row's data record + * @param {Array} lockConditions - A set of one or more Boolean values (or ones that cast correctly to the expected Boolean value) derived from the row record or other values that indicate whether or not the subject cell should be marked as locked + * @param {String} lockedClasses - One or more css class names + * @param {Boolean} conditionsRequireAll - Whether all passed lockConditions need to evaluate to true to apply the locked class(es) + */ + setEditableCellClasses: function(record, lockConditions = [], lockedClasses = '', conditionsRequireAll = true) { + const + userCanEditRecord = this.userCanEditRecord(record), + lockedCSS = lockedClasses || 'locked' + ; + let + classes = '', + shouldLock = false + ; + if (lockConditions.length > 0) { + shouldLock = conditionsRequireAll + ? lockConditions.every(condition => Boolean(condition) === true) + : lockConditions.some(condition => Boolean(condition) === true) + ; } - return z; + if (!this.userCanEdit || !this.userHasRecordPermissions(record) || !userCanEditRecord) { + classes = 'editor-disabled'; + } else if (userCanEditRecord && shouldLock) { + classes = lockedCSS; + } + return classes; }, /** @@ -651,7 +601,7 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { } }, - /* User Group-Level Permissions Checks for the calling "class" object */ + // -> User Group-Level Permissions Checks for the calling "class" object /** * @property {Function} setUserCanEdit - Assigns a value to userCanEdit property based on @@ -706,7 +656,7 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { } }, - /* Record-Level Permissions Checks, for objects with specific policies */ + // -> Record-Level Permissions Checks, for objects with specific policies userHasRecordPermissions: function(record) { const objPermissions = record.json.permissions; @@ -731,30 +681,6 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { return !Ext.isEmpty(objPermissions) && objPermissions.duplicate === true; }, - /** - * @property {Function} setShowActionsMenu - Based on properties set in the calling child class and the - * the current user's permissions for actions taken within that class (create, edit, delete, etc), - * evaluates whether the actions menu trigger should appear and sets boolean value on the showActionsMenu property - * - * @return void - */ - setShowActionsMenu: function() { - if (this.config.disableContextMenuAction === true) { - this.showActionsMenu = false; - return; - } - const permissionsValues = []; - this.gridMenuActions.forEach(mode => { - mode = mode === 'duplicate' ? 'userCanCreate' : `userCan${Ext.util.Format.capitalize(mode)}`; - const modePermission = mode === 'userCanExport' ? true : this[mode]; - if (['userCanCreate', 'userCanEdit'].includes(mode) && modePermission === true) { - this.userHasSavePermissions = true; - } - permissionsValues.push(modePermission); - }); - this.showActionsMenu = !(permissionsValues.length === 0 || permissionsValues.every(value => value === false) === true); - }, - /** * @property {Function} recordIsProtected - Used to remove the ability to delete * specific record rows, regardless of permissions levels, based on a given record identifier @@ -788,33 +714,255 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { return reservedValues.some(reserved => reserved.toLowerCase() === value.toLowerCase()); }, + // -*-*-* GRID FILTERING *-*-*- + /** - * @property {Function} getRemovableItemsFromSelection - Prunes protected items from the current - * selection list before submitting for deletion, or for setting the state of the 'Delete Selected' - * menu item + * @property {Function} findTabPanel - Recursively search ownerCts for this component's enclosing TabPanel * - * @param {String} itemIdType - The data type of the value being inspected (either string or integer) + * @param {Object} referenceCmp - A child component of the TabPanel we're looking for + * @return Ext.TabPanel + */ + findTabPanel: function(referenceCmp) { + if (!Object.hasOwn(referenceCmp, 'ownerCt')) { + console.error('MODx.grid.GridBase::findTabPanel: This component must have an ownerCt to find its tab panel.'); + return false; + } + const container = referenceCmp.ownerCt, + isTabPanel = Object.hasOwn(container, 'xtype') && container.xtype.includes('tabs') + ; + if (isTabPanel) { + return container; + } + return this.findTabPanel(container); + }, + + /** + * Applies a value persisted via URL (MODx.request) for use in grid and filter params. Used when multiple + * grids make use of the same data point, but the request value should apply to only one of them. + * (Primary use-case is in the User Group Access Permissions area.) * - * @return {Array} + * @param {Number} tabPanelIndex The zero-based index of the tab panel containing this grid + * @param {String} requestKey The data point (policy, namespace, etc) + * @param {String} tabPanelType The panel type this grid is a child of + * @param {Boolean} setEmptyToString - For some components, like combos, setting to null is better + * when no value is present. Set this to true for components that prefer an empty string + * @returns {Number|String} Decoded param value */ - getRemovableItemsFromSelection: function(itemIdType = 'int') { - const selections = this.getSelectionModel().getSelections(), - pk = this.config.primaryKey || 'id', - removableItems = [] + applyRequestFilter: function(tabPanelIndex, requestKey = 'policy', tabPanelType = 'vtab', setEmptyToString = false) { + const emptyVal = setEmptyToString ? '' : null; + return Object.prototype.hasOwnProperty.call(MODx.request, tabPanelType) + && parseInt(MODx.request[tabPanelType], 10) === tabPanelIndex + && Object.prototype.hasOwnProperty.call(MODx.request, requestKey) + ? MODx.util.url.getParamValue(requestKey) + : emptyVal ; - if (selections.length <= 0) { - return []; + }, + + /** + * Filters the grid data by the passed filter component (field) + * + * @param {Object} cmp - The filter field's Ext.Component object + * @param {String} param - The record index to apply the filter on; + * may also be the general query/search field name. + */ + applyGridFilter: function(cmp, param = 'query') { + const filterValue = cmp.getValue(), + store = this.getStore(), + urlParams = {}, + tabPanel = this.findTabPanel(this), + bottomToolbar = this.getBottomToolbar() + ; + let hasParentTabPanel = false, + parentTabItems, + activeParentTabIdx + ; + if (!Ext.isEmpty(filterValue)) { + // Add param to URL when filter has a value + urlParams[param] = filterValue; + } else if (MODx.request[param]) { + /* + Maintain params in URL when already present in URL. Prevents removal of + filter params when reloading or navigating to a URL that includes filter params. + */ + urlParams[param] = MODx.request[param]; + } else { + MODx.util.url.clearParam(cmp); } - selections.forEach(record => { - const deletableRecord = record.json.permissions.delete; - if (!record.json.isProtected && deletableRecord) { - const item = itemIdType === 'string' ? record.data[pk] : parseInt(record.data[pk], 10); - removableItems.push(item); + if (param === 'ns') { + store.baseParams.namespace = filterValue; + } else { + store.baseParams[param] = filterValue; + } + if (tabPanel) { + /* + Determine if this is a vertical tab panel; if so there will also be a + horizontal parent tab panel that needs to be accounted for + */ + if (tabPanel.xtype === 'modx-vtabs') { + const parentTabPanel = this.findTabPanel(tabPanel); + if (parentTabPanel) { + const activeParentTab = parentTabPanel.getActiveTab(); + hasParentTabPanel = true; + parentTabItems = parentTabPanel.items; + activeParentTabIdx = parentTabItems.indexOf(activeParentTab); + } + } + const activeTab = tabPanel.getActiveTab(), + tabItems = tabPanel.items, + activeTabIdx = tabItems.indexOf(activeTab) + ; + // Only need to add tab index to the URL when there are multiple tabs + if (hasParentTabPanel) { + if (tabItems.length > 1) { + urlParams.vtab = activeTabIdx; + } + if (parentTabItems.length > 1) { + urlParams.tab = activeParentTabIdx; + } + } else if (tabItems.length > 1) { + urlParams.tab = activeTabIdx; + } + } + store.load(); + MODx.util.url.setParams(urlParams); + if (bottomToolbar) { + bottomToolbar.changePage(1); + } + }, + + /** + * @property {Function} clearGridFilters - Clears all grid filters and sets them to their default value + * + * @param {String|Array} items - A comma-separated list (or array) of items to be cleared. An optional default value + * may also be specified. The expected format for each item in the list is: + * 'filter-category', where 'filter-category' matches the Ext component's itemId and 'category' is the record index to filter on OR + * 'filter-category:3', where '3' is the filter's default value to be applied (instead of setting to an empty value) + * + */ + clearGridFilters: function(items) { + const store = this.getStore(), + bottomToolbar = this.getBottomToolbar(), + data = Array.isArray(items) ? items : items.split(',') + ; + data.forEach(item => { + const itemData = item.replace(/\s+/g, '').split(':'), + itemId = itemData[0], + itemDefaultVal = itemData.length === 2 ? itemData[1] : null, + cmp = this.getFilterComponent(itemId), + cmpParam = MODx.util.url.getParamNameFromCmp(cmp), + isCombo = cmp?.xtype?.includes('combo') + ; + if (isCombo) { + if (itemDefaultVal === '') { + cmp.setValue(null); + } else { + cmp.setValue(itemDefaultVal); + } + } else { + cmp.setValue(''); + } + if (!Ext.isEmpty(itemDefaultVal)) { + const paramsList = Object.keys(cmp.baseParams); + paramsList.forEach(param => { + switch (param) { + case 'namespace': + cmp.baseParams[param] = 'core'; + break; + case 'topic': + cmp.baseParams[param] = 'default'; + break; + // no default + } + }); + } + if (isCombo) { + if (cmp.mode !== 'local') { + cmp.getStore().load(); + } } + store.baseParams[cmpParam] = itemDefaultVal; }); - return removableItems; + store.load(); + MODx.util.url.clearAllParams(); + if (bottomToolbar) { + bottomToolbar.changePage(1); + } + }, + + /** + * @property {Function} getFilterComponent - Gets a filter component from the top toolbar by its itemId + * + * @param {String} filterId - The Ext itemId of the filter component to fetch + * @return {Ext.Component} + */ + getFilterComponent: function(filterId) { + const topToolbar = this.getTopToolbar(), + cmp = this.hasNestedFilters && filterId !== 'filter-query' + ? topToolbar.find('itemId', `${filterId}-container`)[0].getComponent(filterId) + : topToolbar.getComponent(filterId) + ; + if (typeof cmp !== 'undefined') { + return cmp; + } + console.error(`getFilterComponent: The filter component with itemId '${filterId}' could not be retrieved.`); + }, + + /** + * @property {Function} refreshFilterOptions - Used to syncronize a filter's store options to those available in its target grid + * + * @param {Array} filterData - An array of objects containing info needed to refresh each filter + * @param {Boolean} clearDependentParams - If true, will clear values of dependentParams specified in the filterData + */ + refreshFilterOptions: function(filterData = [], clearDependentParams = true) { + if (filterData.length > 0) { + filterData.forEach(data => { + const filter = this.getFilterComponent(data.filterId); + if (filter) { + const store = filter.getStore(); + filter.setValue(''); + if (store) { + if (Object.hasOwn(data, 'dependentParams')) { + const dependentParams = Array.isArray(data.dependentParams) + ? data.dependentParams + : data.dependentParams.split(',') + ; + dependentParams.forEach(param => { + if (clearDependentParams && Object.hasOwn(store.baseParams, param)) { + store.baseParams[param] = ''; + } + }); + } + store.load(); + } + } + }); + this.refresh(); + } + }, + + /** + * @property {Function} updateDependentFilter - Reloads a related filter's store based on the current filter's selected item + * + * @param {String} filterId - The Ext id of the filter to update + * @param {String} paramKey - Filter baseParams property + * @param {Mixed} paramValue - Filter baseParams value for the paramKey + * @param {Boolean} clearValue - Set true to clear filter's selected value + */ + updateDependentFilter: function(filterId, paramKey, paramValue, clearValue = false) { + const filter = this.getFilterComponent(filterId), + filterStore = filter ? filter.getStore() : null + ; + if (filterStore && typeof paramKey == 'string') { + if (clearValue) { + filter.setValue(''); + } + filterStore.baseParams[paramKey] = paramValue; + filterStore.load(); + } }, + // -*-*-* RENDERERS & ENCODING *-*-*- + renderEditableColumn: function(renderer) { return function(value, metaData, record, rowIndex, colIndex, store) { if (renderer) { @@ -832,12 +980,41 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { }; }, - rendYesNo: function(v, metaData) { - if (v === 1 || v === '1') { v = true; } - if (v === 0 || v === '0') { v = false; } - switch (v) { - case true: - case 'true': + /** + * @property {Function} renderLink - Wraps a grid value with a real or simulated link — a trigger that appears + * like an anchor link, usually to access a dropdown chooser or other control + * + * @param {String} content - The value being wrapped + * @param {Object} attributes - Html attributes to add to the link's tag + * @param {Boolean} isSimulated - Indicates whether the link is real (anchor tag) or not (simulated) + * @param {String} isSimulatedTag - The html tag name to wrap the content with + * + * @return {String} + */ + renderLink: function(content, attributes = {}, isSimulated = false, isSimulatedTag = 'span') { + const + tag = isSimulated ? isSimulatedTag : 'a', + classes = isSimulated ? 'x-grid-link simulated-link' : 'x-grid-link', + el = new Ext.Element(document.createElement(tag)) + ; + el.addClass(classes); + // Add default title if none given in attributes + if (!Object.hasOwn(attributes, 'title')) { + attributes.title = _('edit'); + } + Object.entries(attributes).forEach(([attr, value]) => { + el.dom[attr] = value; + }); + el.dom.innerHTML = Ext.util.Format.htmlEncode(content); + return el.dom.outerHTML; + }, + + rendYesNo: function(v, metaData) { + if (v === 1 || v === '1') { v = true; } + if (v === 0 || v === '0') { v = false; } + switch (v) { + case true: + case 'true': case 1: metaData.css = 'green'; return _('yes'); @@ -851,99 +1028,12 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { } }, - /* Depricated; remove once all grids with bulk deletion capability have been converted */ - getSelectedAsList: function() { - const sels = this.getSelectionModel().getSelections(); - if (sels.length <= 0) { return false; } - - let cs = ''; - for (let i = 0; i < sels.length; i++) { - cs += `,${sels[i].data[this.config.primaryKey || 'id']}`; - } - - if (cs[0] === ',') { - cs = cs.substr(1); - } - return cs; - }, - - /** - * Performs the removal of one or more items selected - * - * @param {String} gridName The object identifier (e.g., 'source', 'context', etc) - * @param {String} removeAction The remove processor to call - * @param {String} pkType Indicates the primary key data type (string or int) - */ - removeSelected: function(gridName, removeAction, pkType = 'int') { - const removableSelections = this.getRemovableItemsFromSelection(pkType); - let - modalText, - actionKey - ; - if (removableSelections.length === 0) { - return false; - } - if (removableSelections.length === 1) { - modalText = _(`${gridName}_remove_confirm`, { name: removableSelections[0] }) || _('confirm_remove'); - } else { - modalText = _(`${gridName}_remove_multiple_confirm`) || _('confirm_remove_multiple'); - } - switch (gridName) { - case 'policy_template': - actionKey = 'templates'; - break; - default: - actionKey = gridName.endsWith('y') - ? `${gridName.substring(0, gridName.length - 1)}ies` - : `${gridName}s` - ; - } - MODx.msg.confirm({ - title: _('selected_remove'), - text: modalText, - url: this.config.url, - params: { - action: removeAction, - [actionKey]: removableSelections.join(',') - }, - listeners: { - success: { - fn: function(response) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - this.fireEvent('afterRemoveRow', { ...response, itemsRemoved: removableSelections }); - }, - scope: this - } - } - }); - return true; - }, - - editorYesNo: function(r = {}) { - Ext.applyIf(r, { - store: new Ext.data.SimpleStore({ - fields: ['d', 'v'], - data: [[_('yes'), true], [_('no'), false]] - }), - displayField: 'd', - valueField: 'v', - mode: 'local', - triggerAction: 'all', - editable: false, - selectOnFocus: false - }); - return new Ext.form.ComboBox(r); - }, - - encodeModified: function() { - const p = this.getStore().getModifiedRecords(), - rs = {} - ; - for (let i = 0; i < p.length; i++) { - rs[p[i].data[this.config.primaryKey || 'id']] = p[i].data; + rendPassword: function(v) { + let z = ''; + for (let i = 0; i < v.length; i++) { + z = `${z}*`; } - return Ext.encode(rs); + return z; }, encode: function() { @@ -955,583 +1045,181 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { return Ext.encode(rs); }, - expandAll: function() { - const expander = this.findExpanderPlugin(this.config.plugins); - - if (!expander) { - return false; - } - - const rows = this.getView().getRows(); - - for (let i = 0; i < rows.length; i++) { - expander.expandRow(rows[i]); - } - - if (this.tools.plus !== undefined) { - this.tools.plus.hide(); - } - - if (this.tools.minus !== undefined) { - this.tools.minus.show(); + /** + * @property {Function} getLinkTemplate - Adds a link on a grid column's value based on the passed params. + * Usage of this method is necessary for grouping grids, where usage of renderers on its column model + * interfere with the grouping functionality. + * + * @param {String} controllerPath - The initial part of the URL query indicating the controller action + * @param {String} displayValueIndex - The data index used as the link's text + * @param {Object} options - Additional URL query parameters (linkParams) and attributes for the link's anchor tag + * @return {Ext.Template} + */ + getLinkTemplate: function(controllerPath, displayValueIndex, options = {}) { + /* + linkParams, if given, should be an array of objects in the following format: + [{ key: 'paramKey', valueIndex: 'paramValue' }, ...{}] + */ + Ext.applyIf(options, { + linkParams: [], + linkClass: 'x-grid-link', + linkTitle: _('edit'), + linkTarget: '_blank' + }); + let params = ''; + controllerPath = controllerPath.indexOf('?a=') === 0 ? controllerPath : `?a=${controllerPath}`; + if (options.linkParams.length > 0) { + params = []; + options.linkParams.forEach(param => { + params.push(`${param.key}={${param.valueIndex}}`); + }); + params = `&${params.join('&')}`; } - - return true; + return new Ext.Template( + `{${displayValueIndex}:htmlEncode}`, + { compiled: true } + ); }, - collapseAll: function() { - const expander = this.findExpanderPlugin(this.config.plugins); - - if (!expander) { - return false; - } - - const rows = this.getView().getRows(); - - for (let i = 0; i < rows.length; i++) { - expander.collapseRow(rows[i]); - } - - if (this.tools.minus !== undefined) { - this.tools.minus.hide(); - } - - if (this.tools.plus !== undefined) { - this.tools.plus.show(); - } - - return true; - }, + // -*-*-* CONFIG & COMPONENT BUILDERS *-*-*- /** - * Returns first found expander plugin - * @param plugins + * Gets the view configuration for grids having row-specific editing permissions + * @param {Boolean} hasBulkActions Whether the grid has a bulk actions option + * (uses the checkbox selection model to select multiple rows) + * @param {Boolean} hasObjectLevelPermissions Whether individual rows might have + * differing permissions, based on the specific object they represent + * @param {Boolean} markActiveRows Whether classes should be added for objects + * whose records that can be activated or deactivated (e.g., Form Customization, Users, etc.) + * @returns {Object} The complete view config */ - findExpanderPlugin: function(plugins) { - if (Ext.isObject(plugins)) { - plugins = [plugins]; - } - - const index = Ext.each(plugins, function(item) { - if (item.id !== undefined && item.id === 'expander') { - return false; + getViewConfig: function(hasBulkActions = true, hasObjectLevelPermissions = true, markActiveRows = false) { + return { + forceFit: true, + scrollOffset: 0, + enableRowBody: true, + showPreview: true, + getRowClass: function(record, index, rowParams, store) { + const + canDeleteRecord = this.grid.userCanDeleteRecord(record), + rowClasses = [] + ; + // Initial class required for grids utilizing the row expander + if (this.cm && Object.hasOwn(this.cm.config[0], 'expandRow')) { + rowClasses.push('x-grid3-row-collapsed'); + } + // Objects whose records can be activated/deactivated do not depend upon permission to delete + if (markActiveRows && Object.hasOwn(record.data, 'active')) { + const activeClass = record.data.active ? 'grid-row-active' : 'grid-row-inactive'; + rowClasses.push(activeClass); + } + // Early return if no deletion restrictions are in effect + if (hasObjectLevelPermissions && canDeleteRecord) { + return rowClasses.length ? rowClasses.join(' ') : '' ; + } + // Add various classes marking a row as protected + if (hasBulkActions && !canDeleteRecord && !markActiveRows) { + rowClasses.push('disable-selection'); + } + if (record.json.isProtected) { + rowClasses.push('modx-protected-row'); + } + return rowClasses.length ? rowClasses.join(' ') : '' ; } - }); - - return plugins[index]; - }, - - _getActionsColumnTpl: function() { - return new Ext.XTemplate('' - + '' - + '
            ' - + '' - + '
          • ' - + '
            ' - + '
          ' - + '
          ' - + '
          ', { - compiled: true - }); - }, - - actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { - /* - Note: To maintain backward compatibility for core grids that have not yet been updated - to the new permissions checks and for extras that may extend this class in their grids, - we check showActionsMenu for strict boolean values (which will only be set by grids using - the new checks); otherwise showActionsMenu will be null (its default value set above), - indicating the legacy checks are to be used. - */ - if (this.showActionsMenu === false) { - return; - } - /* - showActionsMenu will be true if at least one user group-level permission is granted, - excluding create/new permissions (since that is not executed by our context/actions menus). - */ - if (this.showActionsMenu) { - const { isProtected } = record.json; - // Export is always available; only continue filtering if grid does not offer export - if (!this.gridMenuActions.includes('export')) { - if (!this.userHasSavePermissions && isProtected) { - return; - } - // Checking record-level permissions; this block checking for 'cls' can be removed once all grids are updated - if (Object.hasOwn(record.data, 'cls')) { - if (Ext.isEmpty(record.data.cls)) { - return; - } - } - if (Object.hasOwn(record.json, 'permissions')) { - if ( - Ext.isEmpty(record.json.permissions) - || Object.values(record.json.permissions).every(permission => !permission) - ) { - return; - } - } - } - } - // eslint-disable-next-line prefer-spread - const actions = this.getActions.apply(this, arguments); - - if (this.config.disableContextMenuAction !== true) { - actions.push({ - text: _('context_menu'), - action: 'contextMenu', - icon: 'gear' - }); - } - return this._getActionsColumnTpl().apply({ - actions: actions - }); + }; }, /** - * @property {Function} renderLink - Wraps a grid value with a real or simulated link — a trigger that appears - * like an anchor link, usually to access a dropdown chooser or other control - * - * @param {String} content - The value being wrapped - * @param {Object} attributes - Html attributes to add to the link's tag - * @param {Boolean} isSimulated - Indicates whether the link is real (anchor tag) or not (simulated) - * @param {String} isSimulatedTag - The html tag name to wrap the content with - * - * @return {String} - */ - renderLink: function(content, attributes = {}, isSimulated = false, isSimulatedTag = 'span') { - const - tag = isSimulated ? isSimulatedTag : 'a', - classes = isSimulated ? 'x-grid-link simulated-link' : 'x-grid-link', - el = new Ext.Element(document.createElement(tag)) - ; - el.addClass(classes); - // Add default title if none given in attributes - if (!Object.hasOwn(attributes, 'title')) { - attributes.title = _('edit'); - } - Object.entries(attributes).forEach(([attr, value]) => { - el.dom[attr] = value; - }); - el.dom.innerHTML = Ext.util.Format.htmlEncode(content); - return el.dom.outerHTML; - }, - - /** - * Deprecated; renamed checkCellIsEditable. Remove in 3.1 - */ - checkEditable: function(e) { - this.checkCellIsEditable(e); - }, - - /** - * Disables cell editor under specified conditions - * @param {Object} e - Ext event object containing references to grid, record, field, value, row (index), column (index), and cancel (set true to cancel edit). - * @return {Boolean} Return false to cancel or true to commit the edit - */ - checkCellIsEditable: function(e) { - const permissions = e.record.data.perm || ''; - if (permissions.indexOf('edit') === -1) { - return false; - } - // Grid-specific conditions - switch (e.grid.xtype) { - case 'modx-grid-role': { - const - isAuthorityField = e.field === 'authority', - roleIsAssigned = e.record.json.isAssigned - ; - if (roleIsAssigned && isAuthorityField) { - return false; - } - break; - } - default: - } - return true; - }, - - /** - * Add one or more classes to a specific Editor Grid cell, typically to indicate a level of restriction - * - * @param {Object} record - The row's data record - * @param {Array} lockConditions - A set of one or more Boolean values (or ones that cast correctly to the expected Boolean value) derived from the row record or other values that indicate whether or not the subject cell should be marked as locked - * @param {String} lockedClasses - One or more css class names - * @param {Boolean} conditionsRequireAll - Whether all passed lockConditions need to evaluate to true to apply the locked class(es) + * Builds the bulk actions button, containing a menu of various actions + * (typically only contains a delete action) + * @param {String} objectType Identifier for object being worked with + * @param {String} deleteAction Processor path for the removal action + * @param {String} pkType Specifies the object's primary key type (int or string) + * @param {...any} moreActions Additional button identifiers or config objects + * to add to the bulk actions menu + * @returns {Object} The complete bulk actions config */ - setEditableCellClasses: function(record, lockConditions = [], lockedClasses = '', conditionsRequireAll = true) { + getBulkActionsButton: function(objectType, deleteAction, pkType = 'int', ...moreActions) { const - userCanEditRecord = this.userCanEditRecord(record), - lockedCSS = lockedClasses || 'locked' - ; - let - classes = '', - shouldLock = false - ; - if (lockConditions.length > 0) { - shouldLock = conditionsRequireAll - ? lockConditions.every(condition => Boolean(condition) === true) - : lockConditions.some(condition => Boolean(condition) === true) - ; - } - if (!this.userCanEdit || !this.userHasRecordPermissions(record) || !userCanEditRecord) { - classes = 'editor-disabled'; - } else if (userCanEditRecord && shouldLock) { - classes = lockedCSS; - } - return classes; - }, - - /** - * @property {Function} getLinkTemplate - Adds a link on a grid column's value based on the passed params. - * Usage of this method is necessary for grouping grids, where usage of renderers on its column model - * interfere with the grouping functionality. - * - * @param {String} controllerPath - The initial part of the URL query indicating the controller action - * @param {String} displayValueIndex - The data index used as the link's text - * @param {Object} options - Additional URL query parameters (linkParams) and attributes for the link's anchor tag - * @return {Ext.Template} - */ - getLinkTemplate: function(controllerPath, displayValueIndex, options = {}) { - /* - linkParams, if given, should be an array of objects in the following format: - [{ key: 'paramKey', valueIndex: 'paramValue' }, ...{}] - */ - Ext.applyIf(options, { - linkParams: [], - linkClass: 'x-grid-link', - linkTitle: _('edit'), - linkTarget: '_blank' - }); - let params = ''; - controllerPath = controllerPath.indexOf('?a=') === 0 ? controllerPath : `?a=${controllerPath}`; - if (options.linkParams.length > 0) { - params = []; - options.linkParams.forEach(param => { - params.push(`${param.key}={${param.valueIndex}}`); - }); - params = `&${params.join('&')}`; - } - return new Ext.Template( - `{${displayValueIndex}:htmlEncode}`, - { compiled: true } - ); - }, - - getActions: function(value, metaData, record, rowIndex, colIndex, store) { - return []; - }, - - onClickHandler: function(e) { - const target = e.getTarget(); - if (!target.classList.contains('x-grid-action')) { return; } - if (!target.dataset.action) { return; } - - let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; - if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { - actionHandler = target.dataset.action; - if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { - return; - } - } - - const record = this.getSelectionModel().getSelected(), - recordIndex = this.store.indexOf(record); - this.menu.record = record.data; - - this[actionHandler](record, recordIndex, e); - }, - - actionContextMenu: function(record, recordIndex, e) { - this._showMenu(this, recordIndex, e); - }, - - makeUrl: function() { - if (Array.isArray(this.config.urlFilters) && this.config.urlFilters.length > 0) { - const s = this.getStore(), - p = { - a: MODx.request.a - }; - if (MODx.request.id) { - p.id = MODx.request.id; - } - if (MODx.request.key) { - p.key = MODx.request.key; - } - for (let i = 0; i < this.config.urlFilters.length; ++i) { - if (Object.hasOwn(s.baseParams, this.config.urlFilters[i]) && s.baseParams[this.config.urlFilters[i]]) { - if (this.config.urlFilters[i] === 'namespace') { - p.ns = s.baseParams[this.config.urlFilters[i]]; - } else { - p[this.config.urlFilters[i]] = s.baseParams[this.config.urlFilters[i]]; - } - } - } - return Ext.urlAppend(MODx.config.manager_url, Ext.urlEncode(p).replace(/%2F/g, '/')); - } - }, - - replaceState: function() { - if (typeof window.history.replaceState !== 'undefined' - && Array.isArray(this.config.urlFilters) - && this.config.urlFilters.length > 0 - ) { - window.history.replaceState(this.getStore().baseParams, document.title, this.makeUrl()); - } - }, - - /** - * @property {Function} findTabPanel - Recursively search ownerCts for this component's enclosing TabPanel - * - * @param {Object} referenceCmp - A child component of the TabPanel we're looking for - * @return Ext.TabPanel - */ - findTabPanel: function(referenceCmp) { - if (!Object.hasOwn(referenceCmp, 'ownerCt')) { - console.error('MODx.grid.Grid::findTabPanel: This component must have an ownerCt to find its tab panel.'); - return false; - } - const container = referenceCmp.ownerCt, - isTabPanel = Object.hasOwn(container, 'xtype') && container.xtype.includes('tabs') - ; - if (isTabPanel) { - return container; - } - return this.findTabPanel(container); - }, - - /** - * @property {Boolean} hasNestedFilters - Indicates whether the top toolbar filter(s) are nested - * within a secondary container; they will be nested when they have labels and those labels are - * positioned above the filter's input. - */ - hasNestedFilters: false, - - currentLanguage: MODx.config.cultureKey || 'en', // removed MODx.request.language - - /** - * Applies a value persisted via URL (MODx.request) for use in grid and filter params. Used when multiple - * grids make use of the same data point, but the request value should apply to only one of them. - * (Primary use-case is in the User Group Access Permissions area.) - * - * @param {Number} tabPanelIndex The zero-based index of the tab panel containing this grid - * @param {String} requestKey The data point (policy, namespace, etc) - * @param {String} tabPanelType The panel type this grid is a child of - * @param {Boolean} setEmptyToString - For some components, like combos, setting to null is better - * when no value is present. Set this to true for components that prefer an empty string - * @returns {Number|String} Decoded param value - */ - applyRequestFilter: function(tabPanelIndex, requestKey = 'policy', tabPanelType = 'vtab', setEmptyToString = false) { - const emptyVal = setEmptyToString ? '' : null; - return Object.prototype.hasOwnProperty.call(MODx.request, tabPanelType) - && parseInt(MODx.request[tabPanelType], 10) === tabPanelIndex - && Object.prototype.hasOwnProperty.call(MODx.request, requestKey) - ? MODx.util.url.getParamValue(requestKey) - : emptyVal - ; - }, - - /** - * Filters the grid data by the passed filter component (field) - * - * @param {Object} cmp - The filter field's Ext.Component object - * @param {String} param - The record index to apply the filter on; - * may also be the general query/search field name. - */ - applyGridFilter: function(cmp, param = 'query') { - const filterValue = cmp.getValue(), - store = this.getStore(), - urlParams = {}, - tabPanel = this.findTabPanel(this), - bottomToolbar = this.getBottomToolbar() - ; - let hasParentTabPanel = false, - parentTabItems, - activeParentTabIdx - ; - if (!Ext.isEmpty(filterValue)) { - // Add param to URL when filter has a value - urlParams[param] = filterValue; - } else if (MODx.request[param]) { - /* - Maintain params in URL when already present in URL. Prevents removal of - filter params when reloading or navigating to a URL that includes filter params. - */ - urlParams[param] = MODx.request[param]; - } else { - MODx.util.url.clearParam(cmp); - } - if (param === 'ns') { - store.baseParams.namespace = filterValue; - } else { - store.baseParams[param] = filterValue; - } - if (tabPanel) { - /* - Determine if this is a vertical tab panel; if so there will also be a - horizontal parent tab panel that needs to be accounted for - */ - if (tabPanel.xtype === 'modx-vtabs') { - const parentTabPanel = this.findTabPanel(tabPanel); - if (parentTabPanel) { - const activeParentTab = parentTabPanel.getActiveTab(); - hasParentTabPanel = true; - parentTabItems = parentTabPanel.items; - activeParentTabIdx = parentTabItems.indexOf(activeParentTab); - } - } - const activeTab = tabPanel.getActiveTab(), - tabItems = tabPanel.items, - activeTabIdx = tabItems.indexOf(activeTab) - ; - // Only need to add tab index to the URL when there are multiple tabs - if (hasParentTabPanel) { - if (tabItems.length > 1) { - urlParams.vtab = activeTabIdx; - } - if (parentTabItems.length > 1) { - urlParams.tab = activeParentTabIdx; - } - } else if (tabItems.length > 1) { - urlParams.tab = activeTabIdx; - } - } - store.load(); - MODx.util.url.setParams(urlParams); - if (bottomToolbar) { - bottomToolbar.changePage(1); - } - }, - - /** - * @property {Function} clearGridFilters - Clears all grid filters and sets them to their default value - * - * @param {String|Array} items - A comma-separated list (or array) of items to be cleared. An optional default value - * may also be specified. The expected format for each item in the list is: - * 'filter-category', where 'filter-category' matches the Ext component's itemId and 'category' is the record index to filter on OR - * 'filter-category:3', where '3' is the filter's default value to be applied (instead of setting to an empty value) - * - */ - clearGridFilters: function(items) { - const store = this.getStore(), - bottomToolbar = this.getBottomToolbar(), - data = Array.isArray(items) ? items : items.split(',') + menuItems = [], + additionalMenuItems = [], + hasMoreActions = moreActions.length > 0 ; - data.forEach(item => { - const itemData = item.replace(/\s+/g, '').split(':'), - itemId = itemData[0], - itemDefaultVal = itemData.length === 2 ? itemData[1] : null, - cmp = this.getFilterComponent(itemId), - cmpParam = MODx.util.url.getParamNameFromCmp(cmp), - isCombo = cmp?.xtype?.includes('combo') - ; - if (isCombo) { - if (itemDefaultVal === '') { - cmp.setValue(null); - } else { - cmp.setValue(itemDefaultVal); - } - } else { - cmp.setValue(''); - } - if (!Ext.isEmpty(itemDefaultVal)) { - const paramsList = Object.keys(cmp.baseParams); - paramsList.forEach(param => { - switch (param) { - case 'namespace': - cmp.baseParams[param] = 'core'; - break; - case 'topic': - cmp.baseParams[param] = 'default'; - break; - // no default - } - }); - } - if (isCombo) { - if (cmp.mode !== 'local') { - cmp.getStore().load(); + if (hasMoreActions) { + /** @var standardButtons Button configs for actions that are used in select grids, such as the Users and Form Customization (Sets) grids */ + const standardButtons = { + activate: { + text: _('selected_activate'), + itemId: 'modx-bulk-menu-opt-activate', + handler: this.activateSelected, + scope: this + }, + deactivate: { + text: _('selected_deactivate'), + itemId: 'modx-bulk-menu-opt-deactivate', + handler: this.deactivateSelected, + scope: this } - } - store.baseParams[cmpParam] = itemDefaultVal; - }); - store.load(); - MODx.util.url.clearAllParams(); - if (bottomToolbar) { - bottomToolbar.changePage(1); - } - }, - - /** - * @property {Function} getFilterComponent - Gets a filter component from the top toolbar by its itemId - * - * @param {String} filterId - The Ext itemId of the filter component to fetch - * @return {Ext.Component} - */ - getFilterComponent: function(filterId) { - const topToolbar = this.getTopToolbar(), - cmp = this.hasNestedFilters && filterId !== 'filter-query' - ? topToolbar.find('itemId', `${filterId}-container`)[0].getComponent(filterId) - : topToolbar.getComponent(filterId) - ; - if (typeof cmp !== 'undefined') { - return cmp; - } - console.error(`getFilterComponent: The filter component with itemId '${filterId}' could not be retrieved.`); - }, - - /** - * @property {Function} refreshFilterOptions - Used to syncronize a filter's store options to those available in its target grid - * - * @param {Array} filterData - An array of objects containing info needed to refresh each filter - * @param {Boolean} clearDependentParams - If true, will clear values of dependentParams specified in the filterData - */ - refreshFilterOptions: function(filterData = [], clearDependentParams = true) { - if (filterData.length > 0) { - filterData.forEach(data => { - const filter = this.getFilterComponent(data.filterId); - if (filter) { - const store = filter.getStore(); - filter.setValue(''); - if (store) { - if (Object.hasOwn(data, 'dependentParams')) { - const dependentParams = Array.isArray(data.dependentParams) - ? data.dependentParams - : data.dependentParams.split(',') - ; - dependentParams.forEach(param => { - if (clearDependentParams && Object.hasOwn(store.baseParams, param)) { - store.baseParams[param] = ''; - } - }); - } - store.load(); + }; + moreActions.forEach(action => { + if (typeof action === 'string') { + const key = action.toLowerCase(); + if (Object.hasOwn(standardButtons, key)) { + additionalMenuItems.push(standardButtons[key]); } } }); - this.refresh(); + menuItems.push(...additionalMenuItems); + menuItems.push('-'); } - }, - - /** - * @property {Function} updateDependentFilter - Reloads a related filter's store based on the current filter's selected item - * - * @param {String} filterId - The Ext id of the filter to update - * @param {String} paramKey - Filter baseParams property - * @param {Mixed} paramValue - Filter baseParams value for the paramKey - * @param {Boolean} clearValue - Set true to clear filter's selected value - */ - updateDependentFilter: function(filterId, paramKey, paramValue, clearValue = false) { - const filter = this.getFilterComponent(filterId), - filterStore = filter ? filter.getStore() : null - ; - if (filterStore && typeof paramKey == 'string') { - if (clearValue) { - filter.setValue(''); + menuItems.push({ + text: _('selected_remove'), + itemId: 'modx-bulk-menu-opt-remove', + handler: this.removeSelected.createDelegate(this, [objectType, deleteAction, pkType]), + scope: this + }); + return { + text: _('bulk_actions'), + menu: menuItems, + listeners: { + render: { + fn: function(btn) { + if ( + (!this.userCanDelete && !hasMoreActions) + || (!this.userCanDelete && !this.userCanEdit && hasMoreActions) + ) { + btn.hide(); + } + }, + scope: this + }, + click: { + fn: function(btn) { + const + removableItems = this.getRemovableItemsFromSelection(pkType), + menuOptRemove = btn.menu.getComponent('modx-bulk-menu-opt-remove') + ; + if (removableItems.length === 0) { + menuOptRemove.disable(); + } else { + menuOptRemove.enable(); + } + if (hasMoreActions) { + const selections = this.getSelectionModel().getSelections(); + additionalMenuItems.forEach(item => { + const itemCmp = btn.menu.getComponent(item.itemId); + if (selections.length === 0) { + itemCmp.disable(); + } else { + itemCmp.enable(); + } + }); + } + }, + scope: this + } } - filterStore.baseParams[paramKey] = paramValue; - filterStore.load(); - } + }; }, /** @@ -1645,429 +1333,577 @@ Ext.extend(MODx.grid.Grid, Ext.grid.EditorGridPanel, { return config; }, + editorYesNo: function(record = {}) { + Ext.applyIf(record, { + store: new Ext.data.SimpleStore({ + fields: ['d', 'v'], + data: [[_('yes'), true], [_('no'), false]] + }), + displayField: 'd', + valueField: 'v', + mode: 'local', + triggerAction: 'all', + editable: false, + selectOnFocus: false + }); + return new Ext.form.ComboBox(record); + }, + + // -*-*-* ACTION HANDLING *-*-*- + /** - * Builds the standard "Creator" column model object. This column displays for - * objects that have built-in system values as well as values installed/entered - * by Extras and/or Users - * @param {String} objectType Identifier for object being worked with - * @returns {Object} The configuration for the "Creator" column + * @deprecated Remove once all grids with bulk deletion capability have been converted */ - getCreatorColumnConfig: function(objectType) { - return { - header: _('grid_column_creator_header'), - dataIndex: 'creator', - id: `modx-${objectType}--creator`, - width: 70, - align: 'center', - tooltip: _('grid_column_creator_description'), - menuDisabled: true - }; + getSelectedAsList: function() { + const sels = this.getSelectionModel().getSelections(); + if (sels.length <= 0) { return false; } + + let cs = ''; + for (let i = 0; i < sels.length; i++) { + cs += `,${sels[i].data[this.config.primaryKey || 'id']}`; + } + + if (cs[0] === ',') { + cs = cs.substr(1); + } + return cs; }, /** - * Builds the bulk actions button, containing a menu of various actions - * (typically only contains a delete action) - * @param {String} objectType Identifier for object being worked with - * @param {String} deleteAction Processor path for the removal action - * @param {String} pkType Specifies the object's primary key type (int or string) - * @param {...any} moreActions Additional button identifiers or config objects - * to add to the bulk actions menu - * @returns {Object} The complete bulk actions config + * @property {Function} getRemovableItemsFromSelection - Prunes protected items from the current + * selection list before submitting for deletion, or for setting the state of the 'Delete Selected' + * menu item + * + * @param {String} itemIdType - The data type of the value being inspected (either string or integer) + * + * @return {Array} */ - getBulkActionsButton: function(objectType, deleteAction, pkType = 'int', ...moreActions) { - const - menuItems = [], - additionalMenuItems = [], - hasMoreActions = moreActions.length > 0 + getRemovableItemsFromSelection: function(itemIdType = 'int') { + const selections = this.getSelectionModel().getSelections(), + pk = this.config.primaryKey || 'id', + removableItems = [] ; - if (hasMoreActions) { - /** @var standardButtons Button configs for actions that are used in select grids, such as the Users and Form Customization (Sets) grids */ - const standardButtons = { - activate: { - text: _('selected_activate'), - itemId: 'modx-bulk-menu-opt-activate', - handler: this.activateSelected, - scope: this - }, - deactivate: { - text: _('selected_deactivate'), - itemId: 'modx-bulk-menu-opt-deactivate', - handler: this.deactivateSelected, - scope: this - } - }; - moreActions.forEach(action => { - if (typeof action === 'string') { - const key = action.toLowerCase(); - if (Object.hasOwn(standardButtons, key)) { - additionalMenuItems.push(standardButtons[key]); - } - } - }); - menuItems.push(...additionalMenuItems); - menuItems.push('-'); + if (selections.length <= 0) { + return []; } - menuItems.push({ - text: _('selected_remove'), - itemId: 'modx-bulk-menu-opt-remove', - handler: this.removeSelected.createDelegate(this, [objectType, deleteAction, pkType]), - scope: this + selections.forEach(record => { + const deletableRecord = record.json.permissions.delete; + if (!record.json.isProtected && deletableRecord) { + const item = itemIdType === 'string' ? record.data[pk] : parseInt(record.data[pk], 10); + removableItems.push(item); + } }); - return { - text: _('bulk_actions'), - menu: menuItems, - listeners: { - render: { - fn: function(btn) { - if ( - (!this.userCanDelete && !hasMoreActions) - || (!this.userCanDelete && !this.userCanEdit && hasMoreActions) - ) { - btn.hide(); - } - }, - scope: this - }, - click: { - fn: function(btn) { - const - removableItems = this.getRemovableItemsFromSelection(pkType), - menuOptRemove = btn.menu.getComponent('modx-bulk-menu-opt-remove') - ; - if (removableItems.length === 0) { - menuOptRemove.disable(); - } else { - menuOptRemove.enable(); - } - if (hasMoreActions) { - const selections = this.getSelectionModel().getSelections(); - additionalMenuItems.forEach(item => { - const itemCmp = btn.menu.getComponent(item.itemId); - if (selections.length === 0) { - itemCmp.disable(); - } else { - itemCmp.enable(); - } - }); - } + return removableItems; + }, + + /** + * Performs the removal of one or more items selected + * + * @param {String} gridName The object identifier (e.g., 'source', 'context', etc) + * @param {String} removeAction The remove processor to call + * @param {String} pkType Indicates the primary key data type (string or int) + */ + removeSelected: function(gridName, removeAction, pkType = 'int') { + const removableSelections = this.getRemovableItemsFromSelection(pkType); + let + modalText, + actionKey + ; + if (removableSelections.length === 0) { + return false; + } + if (removableSelections.length === 1) { + modalText = _(`${gridName}_remove_confirm`, { name: removableSelections[0] }) || _('confirm_remove'); + } else { + modalText = _(`${gridName}_remove_multiple_confirm`) || _('confirm_remove_multiple'); + } + switch (gridName) { + case 'policy_template': + actionKey = 'templates'; + break; + default: + actionKey = gridName.endsWith('y') + ? `${gridName.substring(0, gridName.length - 1)}ies` + : `${gridName}s` + ; + } + MODx.msg.confirm({ + title: _('selected_remove'), + text: modalText, + url: this.config.url, + params: { + action: removeAction, + [actionKey]: removableSelections.join(',') + }, + listeners: { + success: { + fn: function(response) { + this.getSelectionModel().clearSelections(true); + this.refresh(); + this.fireEvent('afterRemoveRow', { ...response, itemsRemoved: removableSelections }); }, scope: this } } - }; + }); + return true; }, - /** - * Gets the view configuration for grids having row-specific editing permissions - * @param {Boolean} hasBulkActions Whether the grid has a bulk actions option - * (uses the checkbox selection model to select multiple rows) - * @param {Boolean} hasObjectLevelPermissions Whether individual rows might have - * differing permissions, based on the specific object they represent - * @param {Boolean} markActiveRows Whether classes should be added for objects - * whose records that can be activated or deactivated (e.g., Form Customization, Users, etc.) - * @returns {Object} The complete view config - */ - getViewConfig: function(hasBulkActions = true, hasObjectLevelPermissions = true, markActiveRows = false) { - return { - forceFit: true, - scrollOffset: 0, - getRowClass: function(record, index, rowParams, store) { - const - canDeleteRecord = this.grid.userCanDeleteRecord(record), - rowClasses = [] - ; - // Objects whose records can be activated/deactivated do not depend upon permission to delete - if (markActiveRows && Object.hasOwn(record.data, 'active')) { - const activeClass = record.data.active ? 'grid-row-active' : 'grid-row-inactive'; - rowClasses.push(activeClass); - } - // Early return if no deletion restrictions are in effect - if (hasObjectLevelPermissions && canDeleteRecord) { - return rowClasses.length ? rowClasses.join(' ') : '' ; - } - // Add various classes marking a row as protected - if (hasBulkActions && !canDeleteRecord && !markActiveRows) { - rowClasses.push('disable-selection'); - } - if (record.json.isProtected) { - rowClasses.push('modx-protected-row'); - } - return rowClasses.length ? rowClasses.join(' ') : '' ; - } - }; - } -}); + confirm: function(type, text) { + const + p = { action: type }, + k = this.config.primaryKey || 'id' + ; + p[k] = this.menu.record[k]; -/* - Local Grid, used by: - - FC Profile Set TVs grid - - Element Properties grid - - Element Sources grid - - Source Properties - - Source Access Permissions - - Resource, Resource Groups (security) grid - - User, Access Permissions (user-groups) - - Dashboard Widget, Dashboards grid (modx-grid-dashboard-widget-dashboards) - - Dashboards (modx-grid-dashboard-widget-placements) -*/ -MODx.grid.LocalGrid = function(config = {}) { - if (config.grouping) { - Ext.applyIf(config, { - view: new Ext.grid.GroupingView({ - forceFit: true, - scrollOffset: 0, - hideGroupedColumn: config.hideGroupedColumn, - groupTextTpl: config.groupTextTpl || (`{text} ({[values.rs.length]} {[values.rs.length > 1 ? "${ - config.pluralText || _('records')}" : "${ - config.singleText || _('record')}"]})`) - }) + MODx.msg.confirm({ + title: _(type), + text: _(text) || _('confirm_remove'), + url: this.config.url, + params: p, + listeners: { + success: { fn: this.refresh, scope: this } + } }); - } - if (config.tbar) { - for (let i = 0; i < config.tbar.length; i++) { - const itm = config.tbar[i]; - if (itm.handler && typeof (itm.handler) == 'object' && itm.handler.xtype) { - itm.handler = this.loadWindow.createDelegate(this, [itm.handler], true); + }, + + remove: function(text, action) { + if (this.destroying) { + return MODx.grid.Grid.superclass.remove.apply(this, arguments); + } + const r = this.menu.record; + text = text || 'confirm_remove'; + const p = this.config.saveParams || {}; + Ext.apply(p, { action: action || 'remove' }); + const k = this.config.primaryKey || 'id'; + p[k] = r[k]; + + if (this.fireEvent('beforeRemoveRow', r)) { + MODx.msg.confirm({ + title: _('warning'), + text: _(text, r), + url: this.config.url, + params: p, + listeners: { + success: { + fn: function() { + this.removeActiveRow(r); + }, + scope: this + } + } + }); + } + }, + + removeActiveRow: function(r) { + if (this.fireEvent('afterRemoveRow', r)) { + const rx = this.getSelectionModel().getSelected(); + this.getStore().remove(rx); + } + }, + + refresh: function() { + this.getStore().reload(); + }, + + // -*-*-* EVENT HANDLERS *-*-*- + + onClickHandler: function(e) { + const target = e.getTarget(); + if (!target.classList.contains('x-grid-action')) { + return; + } + if (!target.dataset.action) { + return; + } + let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; + + if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { + actionHandler = target.dataset.action; + if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { + return; } - if (!itm.scope) { itm.scope = this; } } - } - Ext.applyIf(config, { - title: '', - store: this._loadStore(config), - sm: new Ext.grid.RowSelectionModel({ - singleSelect: false - }), - loadMask: true, - collapsible: true, - stripeRows: true, - enableColumnMove: true, - header: false, - cls: 'modx-grid', - showActionsColumn: true, - actionsColumnWidth: 50, - disableContextMenuAction: false, - viewConfig: { - forceFit: true, - enableRowBody: true, - autoFill: true, - showPreview: true, - scrollOffset: 0, - emptyText: config.emptyText || _('ext_emptymsg') - }, - menuConfig: { defaultAlign: 'tl-b?', enableScrolling: false } - }); + const + record = this.getSelectionModel().getSelected(), + recordIndex = this.store.indexOf(record) + ; + this.menu.record = record.data; - this.menu = new Ext.menu.Menu(config.menuConfig); - this.config = config; - this._loadColumnModel(); + this[actionHandler](record, recordIndex, e); + }, - if (config.showActionsColumn && config.columns && Array.isArray(config.columns)) { - config.columns.push({ - width: config.actionsColumnWidth || 50, - menuDisabled: true, - renderer: { - fn: this.actionsColumnRenderer, - scope: this + onChangePerPage: function(field, newValue) { + if (Ext.isEmpty(newValue)) { + return false; + } + newValue = parseInt(newValue, 10); + this.getBottomToolbar().pageSize = newValue; + this.store.load({ + params: { + start: 0, + limit: newValue } }); } - MODx.grid.LocalGrid.superclass.constructor.call(this, config); - this.addEvents({ - beforeRemoveRow: true, - afterRemoveRow: true +}); + +MODx.grid.Grid = function(config = {}) { + Ext.applyIf(config, { + cm: this.cm, + autoHeight: true, + preventRender: true, + preventSaveRefresh: true, + showPerPage: true, + stateful: false, + groupingConfig: { + enableGroupingMenu: true + } }); - this.on('rowcontextmenu', this._showMenu, this); -}; -Ext.extend(MODx.grid.LocalGrid, Ext.grid.EditorGridPanel, { + MODx.grid.Grid.superclass.constructor.call(this, config); + this.addEvents('afterAutoSave'); + if (this.autosave) { + this.on('afterAutoSave', this.onAfterAutoSave, this); + } + if (!config.preventRender) { + this.render(); + } + this.on({ + render: { + fn: function() { + const topToolbar = this.getTopToolbar(); + if (topToolbar && topToolbar.initialConfig.cls && topToolbar.initialConfig.cls === 'has-nested-filters') { + this.hasNestedFilters = true; + } + }, + scope: this + } + }); + if (config.autosave) { + this.on('afteredit', this.saveRecord, this); + } + + if (config.paging && config.grouping) { + this.getBottomToolbar().bind(this.store); + } - windows: {}, + if (!config.paging && !Object.hasOwn(config, 'pageSize')) { + config.pageSize = 0; + } + this.getStore().load({ + params: { + start: config.pageStart || 0, + limit: Object.hasOwn(config, 'pageSize') ? config.pageSize : (parseInt(MODx.config.default_per_page, 10) || 20) + } + }); + this.getStore().on('exception', this.onStoreException, this); + this.config = config; +}; +Ext.extend(MODx.grid.Grid, MODx.grid.GridBase, { - _loadStore: function(config) { - if (config.grouping) { + _loadStore: function() { + if (this.config.grouping) { this.store = new Ext.data.GroupingStore({ - data: config.data || [], - reader: new Ext.data.ArrayReader({}, config.fields || []), - sortInfo: config.sortInfo || { - field: config.sortBy || 'name', - direction: config.sortDir || 'ASC' + url: this.config.url, + baseParams: this.config.baseParams || { action: this.config.action || 'getList' }, + reader: new Ext.data.JsonReader({ + totalProperty: 'total', + root: 'results', + fields: this.config.fields + }), + sortInfo: { + field: this.config.sortBy || 'id', + direction: this.config.sortDir || 'ASC' }, - groupField: config.groupBy || 'name' + remoteSort: this.config.remoteSort || false, + remoteGroup: this.config.remoteGroup || false, + groupField: this.config.groupBy || 'name', + groupDir: this.config.groupDir || 'ASC', + storeId: this.config.storeId || Ext.id(), + autoDestroy: true, + listeners: { + beforeload: function(store, options) { + const changedGroupDir = store.groupField === store.sortInfo.field && store.groupDir !== store.sortInfo.direction; + if (changedGroupDir) { + store.groupDir = store.sortInfo.direction; + store.baseParams.groupDir = store.sortInfo.direction; + } + }, + load: function(store, records, options) { + const cmp = Ext.getCmp('modx-content'); + if (cmp) { + cmp.doLayout(); + } + }, + groupchange: { + fn: function(store, groupField) { + store.groupDir = this.config.groupDir || 'ASC'; + store.baseParams.groupDir = store.groupDir; + store.sortInfo.direction = this.config.sortDir || 'ASC'; + store.load(); + }, + scope: this + } + } }); } else { - this.store = new Ext.data.SimpleStore({ - fields: config.fields, - data: config.data || [] + this.store = new Ext.data.JsonStore({ + url: this.config.url, + baseParams: this.config.baseParams || { action: this.config.action || 'getList' }, + fields: this.config.fields, + root: 'results', + totalProperty: 'total', + remoteSort: this.config.remoteSort || false, + storeId: this.config.storeId || Ext.id(), + autoDestroy: true, + listeners: { + load: function() { + const cmp = Ext.getCmp('modx-content'); + if (cmp) { + cmp.doLayout(); + } + } + } }); } - return this.store; }, - loadWindow: function(btn, e, win, or) { - const r = this.menu.record; - if (!this.windows[win.xtype]) { - Ext.applyIf(win, { - scope: this, - success: this.refresh, - record: win.blankValues ? {} : r - }); - if (or) { - Ext.apply(win, or); + onStoreException: function(dataProxy, type, action, options, response) { + const responseStatusCode = response.status || 'Unknown', + responseStatusText = !Ext.isEmpty(response.statusText) ? `(${response.statusText})` : '' + ; + let output = '', + msg = '' + ; + if (Ext.isEmpty(response.responseText)) { + // When php display_error is off, responseText will likely be empty and only general status info will be available + output = responseStatusCode !== 200 ? `
          ${responseStatusCode} ${responseStatusText}
          ` : ''; + } else { + // When php display_error is on OR the error is caught and explicity sent from the MODx class triggering the error, responseText should contain error text or possibly an object containing message text + try { + const responseText = Ext.decode(response.responseText); + // In what scenario will responseText be an object with a message property? + if (responseText && responseText.message) { + output = responseText.message; + } + } catch (e) { + output = response.responseText; } - this.windows[win.xtype] = Ext.ComponentMgr.create(win); } - if (this.windows[win.xtype].setValues && win.blankValues !== true && r !== undefined) { - this.windows[win.xtype].setValues(r); + if (output) { + if (MODx.config.debug > 0) { + output = MODx.util.safeHtml(output, '
          ', 'class,colspan,rowspan'); + msg = _('error_grid_get_content_toscreen', { + message: `
          ${output}
          ` + }); + } else { + msg = _('error_grid_get_content_tolog'); + output = Ext.util.Format.stripTags(output).replaceAll('>', '>').replaceAll('<', '<'); + console.error(output); + } + } else { + // With some scenarios, such as when php display_errors = 1 and MODx system setting debug = 0 (reporting off), the reponseText will be empty and the status will be 200 + msg = _('error_grid_get_content_no_msg'); } - this.windows[win.xtype].show(e.target); + this.getView().emptyText = `
          ${msg}
          `; + this.getView().refresh(false); }, - _loadColumnModel: function() { - if (this.config.columns) { - const c = this.config.columns; - for (let i = 0; i < c.length; i++) { - if (typeof (c[i].editor) == 'string') { - // eslint-disable-next-line no-eval - c[i].editor = eval(c[i].editor); - } - if (typeof (c[i].renderer) == 'string') { - // eslint-disable-next-line no-eval - c[i].renderer = eval(c[i].renderer); - } - if (typeof (c[i].editor) == 'object' && c[i].editor.xtype) { - const r = c[i].editor.renderer; - c[i].editor = Ext.ComponentMgr.create(c[i].editor); - if (r === true) { - if (c[i].editor && c[i].editor.store && !c[i].editor.store.isLoaded && c[i].editor.config.mode !== 'local') { - c[i].editor.store.load(); - c[i].editor.store.isLoaded = true; + /** + * Executes auto save of the row after edits are complete and optional success callback + * @param {Ext.Event} e Extended event data including: + * * column + * * row + * * field (name) + * * grid (full grid object) + * * record (full Ext record object including store, data, json, etc.) + * * originalValue + * * value (current) + */ + saveRecord: function(e) { + e.record.data.menu = null; + const p = this.config.saveParams || {}; + Ext.apply(e.record.data, p); + const + data = Ext.util.JSON.encode(e.record.data), + url = this.config.saveUrl || (this.config.url || this.config.connector) + ; + MODx.Ajax.request({ + url: url, + params: { + action: this.config.save_action || 'updateFromGrid', + data: data + }, + listeners: { + success: { + fn: function(response) { + if (this.config.save_callback) { + Ext.callback(this.config.save_callback, this.config.scope || this, [response]); } - c[i].renderer = Ext.util.Format.comboRenderer(c[i].editor); - } else if (c[i].editor.initialConfig.xtype === 'datefield') { - c[i].renderer = Ext.util.Format.dateRenderer(c[i].editor.initialConfig.format || 'Y-m-d'); - } else if (r === 'boolean') { - c[i].renderer = this.rendYesNo; - } else if (r === 'password') { - c[i].renderer = this.rendPassword; - } else if (r === 'local' && typeof (c[i].renderer) == 'string') { - // eslint-disable-next-line no-eval - c[i].renderer = eval(c[i].renderer); - } - } - - /** - * When no renderer is provided, automatically apply the htmlEncode renderer to protect - * against XSS vulnerabilities. Columns that do have a renderer applied are assumed to - * implement their own protection. - */ - if (Ext.isEmpty(c[i].renderer)) { - c[i].renderer = Ext.util.Format.htmlEncode; + e.record.commit(); + if (!this.config.preventSaveRefresh) { + const gridRefresh = new Ext.util.DelayedTask(() => this.refresh()); + gridRefresh.delay(200); + } + const + /** @var {Object} eventData Plucking only the needed event props to forward in the post-save event */ + eventData = { field: e.field, originalValue: e.originalValue, value: e.value }, + responseData = { ...response, eventData } + ; + this.fireEvent('afterAutoSave', responseData); + }, + scope: this + }, + failure: { + fn: function(response) { + e.record.reject(); + this.fireEvent('afterAutoSave', response); + }, + scope: this } + } + }); + }, - /** - * When the field has an editor defined, wrap the (optional) renderer with - * a special renderer that applies a class and tooltip to indicate the - * column is editable. - */ - if (c[i].editor) { - c[i].renderer = this.renderEditableColumn(c[i].renderer); - } + /** + * Method executed after a record has been edited/saved inline from within the grid + * + * @param {Object} response - The processor save response object. See modConnectorResponse::outputContent (PHP) + */ + onAfterAutoSave: function(response) { + if (!response.success && response.message === '') { + let msg = ''; + if (response.data.length) { + // We get some data for specific field(s) error but not regular error message + Ext.each(response.data, function(data, index, list) { + msg += (msg !== '' ? '
          ' : '') + data.msg; + }, this); } - this.cm = new Ext.grid.ColumnModel(c); + if (Ext.isEmpty(msg)) { + // Still no valid message so far, let's use some fallback + msg = this.autosaveErrorMsg || _('error'); + } + MODx.msg.alert(_('error'), msg); } }, - renderEditableColumn: function(renderer) { - return function(value, metaData, record, rowIndex, colIndex, store) { - if (renderer) { - if (typeof renderer.fn === 'function') { - const scope = (renderer.scope) ? renderer.scope : false; - renderer = renderer.fn.bind(scope); - } + encodeModified: function() { + const p = this.getStore().getModifiedRecords(), + rs = {} + ; + for (let i = 0; i < p.length; i++) { + rs[p[i].data[this.config.primaryKey || 'id']] = p[i].data; + } + return Ext.encode(rs); + }, - if (typeof renderer === 'function') { - value = renderer(value, metaData, record, rowIndex, colIndex, store); + makeUrl: function() { + if (Array.isArray(this.config.urlFilters) && this.config.urlFilters.length > 0) { + const s = this.getStore(), + p = { + a: MODx.request.a + }; + if (MODx.request.id) { + p.id = MODx.request.id; + } + if (MODx.request.key) { + p.key = MODx.request.key; + } + for (let i = 0; i < this.config.urlFilters.length; ++i) { + if (Object.hasOwn(s.baseParams, this.config.urlFilters[i]) && s.baseParams[this.config.urlFilters[i]]) { + if (this.config.urlFilters[i] === 'namespace') { + p.ns = s.baseParams[this.config.urlFilters[i]]; + } else { + p[this.config.urlFilters[i]] = s.baseParams[this.config.urlFilters[i]]; + } } } - metaData.css = ['x-editable-column', metaData.css || ''].join(' '); - - return value; - }; + return Ext.urlAppend(MODx.config.manager_url, Ext.urlEncode(p).replace(/%2F/g, '/')); + } }, - _showMenu: function(g, ri, e) { - e.stopEvent(); - e.preventDefault(); - this.menu.recordIndex = ri; - this.menu.record = this.getStore().getAt(ri).data; - if (!this.getSelectionModel().isSelected(ri)) { - this.getSelectionModel().selectRow(ri); - } - this.menu.removeAll(); - const m = this.getMenu(g, ri); - if (m) { - this.addContextMenuItem(m); - this.menu.showAt(e.xy); + replaceState: function() { + if (typeof window.history.replaceState !== 'undefined' + && Array.isArray(this.config.urlFilters) + && this.config.urlFilters.length > 0 + ) { + window.history.replaceState(this.getStore().baseParams, document.title, this.makeUrl()); } }, - getMenu: function() { - return this.menu.record.menu; - }, + /** + * Builds the standard "Creator" column model object. This column displays for + * objects that have built-in system values as well as values installed/entered + * by Extras and/or Users + * @param {String} objectType Identifier for object being worked with + * @returns {Object} The configuration for the "Creator" column + */ + getCreatorColumnConfig: function(objectType) { + return { + header: _('grid_column_creator_header'), + dataIndex: 'creator', + id: `modx-${objectType}--creator`, + width: 70, + align: 'center', + tooltip: _('grid_column_creator_description'), + menuDisabled: true + }; + } - addContextMenuItem: function(items) { - const l = items.length; - for (let i = 0; i < l; i++) { - const options = items[i]; +}); - if (options === '-') { - this.menu.add('-'); - continue; - } - let h = Ext.emptyFn; - if (options.handler) { - // eslint-disable-next-line no-eval - h = eval(options.handler); - if (h && typeof (h) == 'object' && h.xtype) { - h = this.loadWindow.createDelegate(this, [h], true); - } - } else { - h = function(itm) { - const o = itm.options, - { id } = this.menu.record, - w = Ext.get('modx_content'); - if (o.confirm) { - Ext.Msg.confirm('', o.confirm, function(e) { - if (e === 'yes') { - const a = Ext.urlEncode(o.params || { action: o.action }), - s = `?id=${id}&${a}` - ; - if (w === null) { - window.location.href = s; - } else { w.dom.src = s; } - } - }, this); - } else { - const a = Ext.urlEncode(o.params || { action: o.action }), - s = `?id=${id}&${a}`; - if (w === null) { - window.location.href = s; - } else { w.dom.src = s; } - } - }; - } - this.menu.add({ - id: options.id || Ext.id(), - text: options.text, - scope: this, - options: options, - handler: h +/* + Local Grid, used by: + - FC Profile Set Fields, Regions, and TVs grids (3) + - Element Properties grid + - Element Sources grid + - Source Properties + - Source Access Permissions + - Resource, Resource Groups (security) grid + - User, Access Permissions (user-groups) + - Dashboard Widget, Dashboards grid (modx-grid-dashboard-widget-dashboards) + - Dashboards (modx-grid-dashboard-widget-placements) +*/ +MODx.grid.LocalGrid = function(config = {}) { + Ext.applyIf(config, { + title: '', + enableColumnMove: true, + //* * NEW + groupingConfig: { + hideGroupedColumn: config.hideGroupedColumn + } + }); + MODx.grid.LocalGrid.superclass.constructor.call(this, config); +}; + +Ext.extend(MODx.grid.LocalGrid, MODx.grid.GridBase, { + + _loadStore: function() { + if (this.config.grouping) { + this.store = new Ext.data.GroupingStore({ + data: this.config.data || [], + reader: new Ext.data.ArrayReader({}, this.config.fields || []), + sortInfo: this.config.sortInfo || { + field: this.config.sortBy || 'name', + direction: this.config.sortDir || 'ASC' + }, + groupField: this.config.groupBy || 'name' + }); + } else { + this.store = new Ext.data.SimpleStore({ + fields: this.config.fields, + data: this.config.data || [] }); } + return this.store; }, + /** + * @override + */ remove: function(config) { if (this.destroying) { return MODx.grid.LocalGrid.superclass.remove.apply(this, arguments); @@ -2083,6 +1919,9 @@ Ext.extend(MODx.grid.LocalGrid, Ext.grid.EditorGridPanel, { } }, + /** + * @override + */ encode: function() { const s = this.getStore(), ct = s.getCount(), @@ -2099,166 +1938,6 @@ Ext.extend(MODx.grid.LocalGrid, Ext.grid.EditorGridPanel, { } return Ext.encode(rs); - }, - - expandAll: function() { - const expander = this.findExpanderPlugin(this.config.plugins); - - if (!expander) { - return false; - } - - const rows = this.getView().getRows(); - - for (let i = 0; i < rows.length; i++) { - expander.expandRow(rows[i]); - } - - if (this.tools.plus !== undefined) { - this.tools.plus.hide(); - } - - if (this.tools.minus !== undefined) { - this.tools.minus.show(); - } - - return true; - }, - - collapseAll: function() { - const expander = this.findExpanderPlugin(this.config.plugins); - - if (!expander) { - return false; - } - - const rows = this.getView().getRows(); - - for (let i = 0; i < rows.length; i++) { - expander.collapseRow(rows[i]); - } - - if (this.tools.minus !== undefined) { - this.tools.minus.hide(); - } - - if (this.tools.plus !== undefined) { - this.tools.plus.show(); - } - - return true; - }, - - /** - * Returns first found expander plugin - * @param plugins - */ - findExpanderPlugin: function(plugins) { - if (Ext.isObject(plugins)) { - plugins = [plugins]; - } - - const index = Ext.each(plugins, function(item) { - if (item.id !== undefined && item.id === 'expander') { - return false; - } - }); - - return plugins[index]; - }, - - rendYesNo: function(d, c) { - switch (d) { - case '': - return '-'; - case false: - c.css = 'red'; - return _('no'); - case true: - c.css = 'green'; - return _('yes'); - // no default - } - }, - - rendPassword: function(v) { - let z = ''; - for (let i = 0; i < v.length; i++) { - z = `${z}*`; - } - return z; - }, - - _getActionsColumnTpl: function() { - return new Ext.XTemplate('' - + '' - + '
            ' - + '' - + '
          • ' - + '
            ' - + '
          ' - + '
          ' - + '
          ', { - compiled: true - }); - }, - - actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { - // eslint-disable-next-line prefer-spread - const actions = this.getActions.apply(this, arguments); - - if (this.config.disableContextMenuAction !== true) { - actions.push({ - text: _('context_menu'), - action: 'contextMenu', - icon: 'gear' - }); - } - - return this._getActionsColumnTpl().apply({ - actions: actions - }); - }, - - renderLink: function(content, attributes) { - const el = new Ext.Element(document.createElement('a')); - el.addClass('x-grid-link'); - if (!Object.hasOwn(attributes, 'title')) { - attributes.title = _('edit'); - } - Object.entries(attributes).forEach(([attr, value]) => { - el.dom[attr] = value; - }); - el.dom.innerHTML = Ext.util.Format.htmlEncode(content); - return el.dom.outerHTML; - }, - - getActions: function(value, metaData, record, rowIndex, colIndex, store) { - return []; - }, - - onClick: function(e) { - const target = e.getTarget(); - if (!target.classList.contains('x-grid-action')) { return; } - if (!target.dataset.action) { return; } - - let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; - if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { - actionHandler = target.dataset.action; - if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { - return; - } - } - - const record = this.getSelectionModel().getSelected(), - recordIndex = this.store.indexOf(record); - this.menu.record = record.data; - - this[actionHandler](record, recordIndex, e); - }, - - actionContextMenu: function(record, recordIndex, e) { - this._showMenu(this, recordIndex, e); } }); Ext.reg('grid-local', MODx.grid.LocalGrid); @@ -2297,7 +1976,7 @@ Ext.ux.grid.RowExpander = Ext.extend(Ext.util.Observable, { expandOnDblClick: true, header: '', - width: 20, + width: 25, sortable: false, fixed: true, hideable: false, @@ -2770,6 +2449,10 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { }); return m; }, + + /** + * @override + */ getActions: function() { return [{ action: 'removeElement', @@ -2777,6 +2460,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { text: _('remove') }]; }, + addElement: function() { const ds = this.getStore(), row = {}; @@ -2789,6 +2473,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { this.getView().refresh(); this.getSelectionModel().selectRow(0); }, + removeElement: function() { Ext.Msg.confirm(_('remove') || '', _('confirm_remove') || '', function(e) { if (e === 'yes') { @@ -2813,6 +2498,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { } }, this); }, + renderListener: function(grid) { new Ext.dd.DropTarget(grid.container, { copy: false, @@ -2840,6 +2526,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { this.add(this.hiddenField); this.saveValue(); }, + loadValue: function(value) { value = Ext.util.JSON.decode(value); if (value && Array.isArray(value)) { @@ -2851,6 +2538,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { } return value; }, + saveValue: function() { const value = []; Ext.each(this.getStore().getRange(), function(record) { @@ -2863,43 +2551,6 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { value.push(row); }, this); this.hiddenField.setValue(Ext.util.JSON.encode(value)); - }, - _getActionsColumnTpl: function() { - return new Ext.XTemplate('' - + '' - + '
            ' - + '' - + '
          • ' - + '
            ' - + '
          ' - + '
          ' - + '
          ', { - compiled: true - }); - }, - actionsColumnRenderer: function(value, metaData, record, rowIndex, colIndex, store) { - return this._getActionsColumnTpl().apply({ - actions: this.getActions() - }); - }, - onClick: function(e) { - const target = e.getTarget(); - if (!target.classList.contains('x-grid-action')) { return; } - if (!target.dataset.action) { return; } - - let actionHandler = `action${target.dataset.action.charAt(0).toUpperCase()}${target.dataset.action.slice(1)}`; - if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { - actionHandler = target.dataset.action; - if (!this[actionHandler] || (typeof this[actionHandler] !== 'function')) { - return; - } - } - - const record = this.getSelectionModel().getSelected(), - recordIndex = this.store.indexOf(record); - this.menu.record = record.data; - - this[actionHandler](record, recordIndex, e); } }); Ext.reg('grid-json', MODx.grid.JsonGrid); From ae546ec38170f27e0a38942976f4aa3bc91354b6 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 14 Nov 2024 22:26:43 -0500 Subject: [PATCH 25/39] Dashboard(s) & Widgets updates Second round/finalization of changes to this area --- .../System/Dashboard/Widget/GetList.php | 61 ++++++- .../system/modx.grid.dashboard.widgets.js | 162 ++++++++---------- .../widgets/system/modx.panel.dashboard.js | 8 +- .../widgets/system/modx.panel.dashboards.js | 9 +- .../system/dashboards/update.class.php | 7 + 5 files changed, 150 insertions(+), 97 deletions(-) diff --git a/core/src/Revolution/Processors/System/Dashboard/Widget/GetList.php b/core/src/Revolution/Processors/System/Dashboard/Widget/GetList.php index 63f300c9db..1525224c8c 100644 --- a/core/src/Revolution/Processors/System/Dashboard/Widget/GetList.php +++ b/core/src/Revolution/Processors/System/Dashboard/Widget/GetList.php @@ -1,4 +1,5 @@ setDefaultProperties([ + 'query' => '', + 'exclude' => 'creator' + ]); + $canManage = $this->modx->hasPermission('dashboards'); + $this->canCreate = $canManage; + $this->canEdit = $canManage; + $this->canRemove = $canManage; + $this->extrasNamespaces = modNamespace::class::getExtrasNamespaces($this->modx); + + return $initialized; + } + /** * {@inheritDoc} * @param xPDOQuery $c @@ -70,8 +97,36 @@ public function prepareQueryAfterCount(xPDOQuery $c) */ public function prepareRow(xPDOObject $object) { - $objectArray = $object->toArray(); - $objectArray['cls'] = 'pupdate premove'; - return $objectArray; + $permissions = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + $widgetData = $object->toArray(); + $widgetNamespace = $object->get('namespace'); + $isCoreWidget = strpos($widgetData['content'], '[[++manager_path]]') === 0; + $widgetData['isExtrasWidget'] = in_array($widgetNamespace, $this->extrasNamespaces); + $widgetData['isProtected'] = true; + + switch (true) { + case $widgetData['isExtrasWidget']: + $widgetData['creator'] = $this->modx->lexicon('package_extra'); + break; + case $isCoreWidget: + $widgetData['creator'] = 'MODX'; + break; + default: + $widgetData['creator'] = $this->modx->lexicon('user'); + $widgetData['isProtected'] = false; + } + $widgetData['creator'] = strtolower($widgetData['creator']); + + if ($isCoreWidget) { + unset($permissions['delete']); + } + $widgetData['permissions'] = $permissions; + + return $widgetData; } } diff --git a/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js b/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js index 6695daad9f..8bcb304c1f 100644 --- a/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js +++ b/manager/assets/modext/widgets/system/modx.grid.dashboard.widgets.js @@ -13,6 +13,7 @@ MODx.grid.DashboardWidgets = function(config = {}) { }); this.sm = new Ext.grid.CheckboxSelectionModel(); Ext.applyIf(config, { + id: 'modx-grid-dashboard-widgets', url: MODx.config.connector_url, baseParams: { action: 'System/Dashboard/Widget/GetList' @@ -28,90 +29,117 @@ MODx.grid.DashboardWidgets = function(config = {}) { 'namespace', 'lexicon', 'size', - 'cls' + 'creator' ], paging: true, remoteSort: true, sm: this.sm, plugins: [this.exp], - columns: [this.exp, this.sm, { - header: _('id'), - dataIndex: 'id', - width: 50, - sortable: true - }, { - header: _('name'), - dataIndex: 'name_trans', - width: 150, - sortable: true, - editable: false, - renderer: { - fn: function(v, md, record) { - return this.renderLink(v, { - href: `?a=system/dashboards/widget/update&id=${record.data.id}` - }); - }, - scope: this - } - }, { - header: _('widget_type'), - dataIndex: 'type', - width: 80, - sortable: true - }, { - header: _('widget_namespace'), - dataIndex: 'namespace', - width: 120, - sortable: true - }], + columns: [ + this.exp, + this.sm, + { + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('name'), + dataIndex: 'name_trans', + width: 150, + sortable: true, + editable: false, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return this.userCanEditRecord(record) + ? this.renderLink(value, { + href: `?a=system/dashboards/widget/update&id=${record.data.id}`, + title: _('dashboard_edit') + }) + : value + ; + }, + scope: this + } + }, { + header: _('widget_type'), + dataIndex: 'type', + width: 80, + sortable: true + }, { + header: _('widget_namespace'), + dataIndex: 'namespace', + width: 120, + sortable: true, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return value; + }, + scope: this + } + }, + this.getCreatorColumnConfig('dashboard') + ], tbar: [ { text: _('create'), cls: 'primary-button', handler: this.createDashboard, scope: this - }, { - text: _('bulk_actions'), - menu: [{ - text: _('selected_remove'), - handler: this.removeSelected, - scope: this - }] }, + this.getBulkActionsButton('widget', 'System/Dashboard/Widget/RemoveMultiple'), '->', this.getQueryFilterField(`filter-query-dashboardWidgets:${queryValue}`), this.getClearFiltersButton('filter-query-dashboardWidgets') ] }); MODx.grid.DashboardWidgets.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete']; + + // Note there are currently no action-specific permissions for Dashboards + this.setUserCanEdit(['dashboards']); + this.setUserCanCreate(['dashboards']); + this.setUserCanDelete(['dashboards']); + this.setShowActionsMenu(); + + this.on({ + beforerender: function(grid) { + grid.view = new Ext.grid.GridView(grid.getViewConfig()); + } + }); }; Ext.extend(MODx.grid.DashboardWidgets, MODx.grid.Grid, { getMenu: function() { const - r = this.getSelectionModel().getSelected(), - p = r.data.cls, - menu = [] + record = this.getSelectionModel().getSelected(), + menu = [], + canDelete = this.userCanDelete && this.userCanDeleteRecord(record) ; - if (this.getSelectionModel().getCount() > 1) { + if (this.getSelectionModel().getCount() > 1 && canDelete) { menu.push({ text: _('selected_remove'), handler: this.removeSelected, scope: this }); } else { - if (p.indexOf('pupdate') !== -1) { + if (this.userCanEdit && this.userCanEditRecord(record)) { menu.push({ text: _('edit'), handler: this.updateWidget }); } - if (p.indexOf('premove') !== -1) { + if (canDelete) { if (menu.length > 0) { menu.push('-'); } menu.push({ text: _('delete'), - handler: this.removeWidget + handler: this.confirm.createDelegate(this, ['System/Dashboard/Widget/Remove', 'widget_remove_confirm']) }); } } @@ -126,50 +154,6 @@ Ext.extend(MODx.grid.DashboardWidgets, MODx.grid.Grid, { updateWidget: function() { MODx.loadPage('system/dashboards/widget/update', `id=${this.menu.record.id}`); - }, - - removeWidget: function() { - MODx.msg.confirm({ - title: _('delete'), - text: _('widget_remove_confirm'), - url: this.config.url, - params: { - action: 'System/Dashboard/Widget/Remove', - id: this.menu.record.id - }, - listeners: { - success: { - fn: this.refresh, - scope: this - } - } - }); - }, - - removeSelected: function() { - const cs = this.getSelectedAsList(); - if (cs === false) { - return false; - } - MODx.msg.confirm({ - title: _('selected_remove'), - text: _('widget_remove_multiple_confirm'), - url: this.config.url, - params: { - action: 'System/Dashboard/Widget/RemoveMultiple', - widgets: cs - }, - listeners: { - success: { - fn: function(r) { - this.getSelectionModel().clearSelections(true); - this.refresh(); - }, - scope: this - } - } - }); - return true; } }); Ext.reg('modx-grid-dashboard-widgets', MODx.grid.DashboardWidgets); diff --git a/manager/assets/modext/widgets/system/modx.panel.dashboard.js b/manager/assets/modext/widgets/system/modx.panel.dashboard.js index 081571983c..287d445cd9 100644 --- a/manager/assets/modext/widgets/system/modx.panel.dashboard.js +++ b/manager/assets/modext/widgets/system/modx.panel.dashboard.js @@ -232,7 +232,7 @@ MODx.grid.DashboardWidgetPlacements = function(config = {}) { Ext.applyIf(config, { id: 'modx-grid-dashboard-widget-placements', url: MODx.config.connector_url, - action: 'system/dashboard/widget/placement/getList', + // action: 'system/dashboard/widget/placement/getList', fields: [ 'dashboard', 'widget', @@ -240,7 +240,8 @@ MODx.grid.DashboardWidgetPlacements = function(config = {}) { 'name', 'name_trans', 'description', - 'description_trans' + 'description_trans', + 'permissions' ], autoHeight: true, primaryKey: 'widget', @@ -294,7 +295,8 @@ MODx.grid.DashboardWidgetPlacements = function(config = {}) { 'name', 'name_trans', 'description', - 'description_trans' + 'description_trans', + 'permissions' ]); }; Ext.extend(MODx.grid.DashboardWidgetPlacements, MODx.grid.LocalGrid, { diff --git a/manager/assets/modext/widgets/system/modx.panel.dashboards.js b/manager/assets/modext/widgets/system/modx.panel.dashboards.js index 216697ba1c..2a0a440775 100644 --- a/manager/assets/modext/widgets/system/modx.panel.dashboards.js +++ b/manager/assets/modext/widgets/system/modx.panel.dashboards.js @@ -164,8 +164,7 @@ MODx.grid.Dashboards = function(config = {}) { }, this.getQueryFilterField(`filter-query:${queryValue}`), this.getClearFiltersButton('filter-usergroup, filter-query') - ], - viewConfig: this.getViewConfig() + ] }); MODx.grid.Dashboards.superclass.constructor.call(this, config); @@ -176,6 +175,12 @@ MODx.grid.Dashboards = function(config = {}) { this.setUserCanCreate(['dashboards']); this.setUserCanDelete(['dashboards']); this.setShowActionsMenu(); + + this.on({ + beforerender: function(grid) { + grid.view = new Ext.grid.GridView(grid.getViewConfig()); + } + }); }; Ext.extend(MODx.grid.Dashboards, MODx.grid.Grid, { getMenu: function() { diff --git a/manager/controllers/default/system/dashboards/update.class.php b/manager/controllers/default/system/dashboards/update.class.php index 790d2c985b..88daf5c8c9 100644 --- a/manager/controllers/default/system/dashboards/update.class.php +++ b/manager/controllers/default/system/dashboards/update.class.php @@ -93,6 +93,12 @@ public function getWidgets() $this->modx->lexicon->load($placement->Widget->get('lexicon')); } $widgetArray = $placement->Widget->toArray(); + // Currently Dashboards do not have action-specific permissions, so hard code them + // here to true since view permission is needed to get to this point + $widgetArray['permissions'] = [ + 'edit' => true, + 'delete' => true + ]; $list[] = [ $placement->get('dashboard'), $placement->get('widget'), @@ -101,6 +107,7 @@ public function getWidgets() $widgetArray['name_trans'], $widgetArray['description'], $widgetArray['description_trans'], + $widgetArray['permissions'] ]; } return $list; From 3b72a6b5579e1f9645e0537485b795165b9001e8 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 14 Nov 2024 22:30:31 -0500 Subject: [PATCH 26/39] Update modx.panel.dashboard.js Remove commented out line --- manager/assets/modext/widgets/system/modx.panel.dashboard.js | 1 - 1 file changed, 1 deletion(-) diff --git a/manager/assets/modext/widgets/system/modx.panel.dashboard.js b/manager/assets/modext/widgets/system/modx.panel.dashboard.js index 287d445cd9..bc0199d2a6 100644 --- a/manager/assets/modext/widgets/system/modx.panel.dashboard.js +++ b/manager/assets/modext/widgets/system/modx.panel.dashboard.js @@ -232,7 +232,6 @@ MODx.grid.DashboardWidgetPlacements = function(config = {}) { Ext.applyIf(config, { id: 'modx-grid-dashboard-widget-placements', url: MODx.config.connector_url, - // action: 'system/dashboard/widget/placement/getList', fields: [ 'dashboard', 'widget', From 7a37bd569fe35f8c28232048ca5608bb6a6bd97f Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 14 Nov 2024 22:48:47 -0500 Subject: [PATCH 27/39] Context Access updates Updates display of and ability to select row actions (gear icon) --- .../Processors/Security/Access/GetList.php | 33 ++++++++++-- .../security/modx.grid.access.context.js | 53 +++++++++++++------ 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/core/src/Revolution/Processors/Security/Access/GetList.php b/core/src/Revolution/Processors/Security/Access/GetList.php index e1d4da8362..847f018ba4 100644 --- a/core/src/Revolution/Processors/Security/Access/GetList.php +++ b/core/src/Revolution/Processors/Security/Access/GetList.php @@ -1,4 +1,5 @@ modx->lexicon('access_type_err_ns'); } + $canManage = $this->modx->hasPermission('access_permissions'); + $this->canCreate = $canManage; + $this->canEdit = $canManage; + $this->canRemove = $canManage; + $this->canEditGroups = $this->modx->hasPermission('usergroup_edit'); + $this->canEditPolicies = $this->modx->hasPermission('policy_edit'); + return parent::initialize(); } @@ -135,7 +149,7 @@ public function prepareRow(xPDOObject $object) $targetName = $this->getAnonymName(); } - $objArray = [ + $accessData = [ 'id' => $object->get('id'), 'target' => $object->get('target'), 'target_name' => $targetName, @@ -148,13 +162,24 @@ public function prepareRow(xPDOObject $object) ]; if (isset($object->_fieldMeta['context_key'])) { - $objArray['context_key'] = $object->get('context_key'); + $accessData['context_key'] = $object->get('context_key'); } // Prevent default Admin ACL from edit and remove - $objArray['cls'] = (($object->get('target') === 'mgr') && ($principal->get('name') === 'Administrator') && ($policyName === 'Administrator') && ($object->get('authority') === 0)) ? '' : 'pedit premove'; + if (($object->get('target') === 'mgr') && ($principal->get('name') === 'Administrator') && ($policyName === 'Administrator') && ($object->get('authority') === 0)) { + $accessData['permissions'] = []; + $accessData['isProtected'] = true; + } else { + $accessData['permissions'] = [ + 'create' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + } + $accessData['canEditGroups'] = $this->canEditGroups; + $accessData['canEditPolicies'] = $this->canEditPolicies; - return $objArray; + return $accessData; } /** diff --git a/manager/assets/modext/widgets/security/modx.grid.access.context.js b/manager/assets/modext/widgets/security/modx.grid.access.context.js index 4a681b8e6e..9e2bae5561 100644 --- a/manager/assets/modext/widgets/security/modx.grid.access.context.js +++ b/manager/assets/modext/widgets/security/modx.grid.access.context.js @@ -23,8 +23,7 @@ MODx.grid.AccessContext = function(config = {}) { 'principal_name', 'authority', 'policy', - 'policy_name', - 'cls' + 'policy_name' ], type: 'modAccessContext', paging: true, @@ -37,11 +36,14 @@ MODx.grid.AccessContext = function(config = {}) { dataIndex: 'principal_name', width: 120, renderer: { - fn: function(value, metadata, record) { - return this.renderLink(value, { - href: `?a=security/usergroup/update&id=${record.data.principal}`, - target: '_blank' - }); + fn: function(value, metaData, record) { + return record.json.canEditGroups + ? this.renderLink(value, { + href: `?a=security/usergroup/update&id=${record.data.principal}`, + target: '_blank' + }) + : value + ; }, scope: this } @@ -54,11 +56,14 @@ MODx.grid.AccessContext = function(config = {}) { dataIndex: 'policy_name', width: 175, renderer: { - fn: function(value, metadata, record) { - return this.renderLink(value, { - href: `?a=security/access/policy/update&id=${record.data.policy}`, - target: '_blank' - }); + fn: function(value, metaData, record) { + return record.json.canEditGroups + ? this.renderLink(value, { + href: `?a=security/access/policy/update&id=${record.data.policy}`, + target: '_blank' + }) + : value + ; }, scope: this } @@ -71,6 +76,20 @@ MODx.grid.AccessContext = function(config = {}) { }] }); MODx.grid.AccessContext.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete']; + + // Note there are currently no action-specific permissions for Access Permissions + this.setUserCanEdit(['access_permissions']); + this.setUserCanCreate(['access_permissions']); + this.setUserCanDelete(['access_permissions']); + this.setShowActionsMenu(); + + this.on({ + beforerender: function(grid) { + grid.view = new Ext.grid.GridView(grid.getViewConfig(false, false)); + } + }); }; Ext.extend(MODx.grid.AccessContext, MODx.grid.Grid, { combos: {}, @@ -79,18 +98,19 @@ Ext.extend(MODx.grid.AccessContext, MODx.grid.Grid, { getMenu: function() { const record = this.getSelectionModel().getSelected(), - p = record.data.cls, menu = [] ; if (this.getSelectionModel().getCount() === 1) { - if (p.indexOf('pedit') !== -1) { + if (this.userCanEdit && this.userCanEditRecord(record)) { menu.push({ text: _('edit'), handler: this.editAcl }); } - if (p.indexOf('premove') !== -1) { - if (menu.length > 0) { menu.push('-'); } + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + if (menu.length > 0) { + menu.push('-'); + } menu.push({ text: _('delete'), handler: this.removeAcl @@ -239,7 +259,6 @@ Ext.reg('modx-window-access-context-create', MODx.window.CreateAccessContext); * @xtype modx-window-access-context-update */ MODx.window.UpdateAccessContext = function(config = {}) { - // var r = config.record; this.ident = config.ident || `uactx${Ext.id()}`; Ext.applyIf(config, { title: _('ugc_mutate'), From 71210c350f4b641a27f9126e9095e2b96b291195 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 15 Nov 2024 01:41:43 -0500 Subject: [PATCH 28/39] Update modx.grid.user.group.js Formatting, code style changes only --- .../widgets/security/modx.grid.user.group.js | 433 ++++++++++-------- 1 file changed, 250 insertions(+), 183 deletions(-) diff --git a/manager/assets/modext/widgets/security/modx.grid.user.group.js b/manager/assets/modext/widgets/security/modx.grid.user.group.js index 26a5648139..630b8d9d70 100644 --- a/manager/assets/modext/widgets/security/modx.grid.user.group.js +++ b/manager/assets/modext/widgets/security/modx.grid.user.group.js @@ -6,161 +6,228 @@ * @param {Object} config An object of options. * @xtype modx-grid-user-groups */ -MODx.grid.UserGroups = function(config) { - config = config || {}; +MODx.grid.UserGroups = function(config = {}) { this.exp = new Ext.grid.RowExpander({ - tpl : new Ext.Template( + tpl: new Ext.Template( '

          {user_group_desc}

          ' ) }); - Ext.applyIf(config,{ - title: '' - ,id: 'modx-grid-user-groups' - ,url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + title: '', + id: 'modx-grid-user-groups', + url: MODx.config.connector_url, + baseParams: { action: 'Security/Group/GetList' - } - ,fields: ['usergroup','name','member','role','rolename','primary_group','rank','user_group_desc'] - ,cls: 'modx-grid modx-grid-draggable' - ,columns: [this.exp, - { - header: _('user_group') - ,dataIndex: 'name' - ,width: 175 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/usergroup/update&id=' + record.data.usergroup - ,target: '_blank' - }); - }, scope: this } - },{ - header: _('role') - ,dataIndex: 'rolename' - ,width: 175 - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=security/permission' - ,target: '_blank' - }); - }, scope: this } - },{ - header: _('rank') - ,dataIndex: 'rank' - ,width: 80 - ,editor: { xtype: 'numberfield', allowBlank: false, allowNegative: false } - }] - ,plugins: [new Ext.ux.dd.GridDragDropRowOrder({ - copy: false - ,scrollable: true - ,targetCfg: {} - ,listeners: { - 'afterrowmove': {fn:this.onAfterRowMove,scope:this} - ,'beforerowmove': {fn:this.onBeforeRowMove,scope:this} + }, + fields: [ + 'usergroup', + 'name', + 'member', + 'role', + 'rolename', + 'primary_group', + 'rank', + 'user_group_desc' + ], + cls: 'modx-grid modx-grid-draggable', + columns: [ + this.exp, + { + header: _('user_group'), + dataIndex: 'name', + width: 175, + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=security/usergroup/update&id=${record.data.usergroup}`, + target: '_blank' + }); + }, + scope: this + } + }, { + header: _('role'), + dataIndex: 'rolename', + width: 175, + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: '?a=security/permission', + target: '_blank' + }); + }, + scope: this + } + }, { + header: _('rank'), + dataIndex: 'rank', + width: 80, + editor: { + xtype: 'numberfield', + allowBlank: false, + allowNegative: false + } } - }), - this.exp] - ,tbar: [{ - text: _('user_group_user_add') - ,cls:'primary-button' - ,handler: this.addGroup + ], + plugins: [ + new Ext.ux.dd.GridDragDropRowOrder({ + copy: false, + scrollable: true, + targetCfg: {}, + listeners: { + afterrowmove: { + fn: this.onAfterRowMove, + scope: this + }, + /** + * @deprecated Appears to be unused + */ + beforerowmove: { + fn: this.onBeforeRowMove, + scope: this + } + } + }), + this.exp + ], + tbar: [{ + text: _('user_group_user_add'), + cls: 'primary-button', + handler: this.addGroup }] }); - MODx.grid.UserGroups.superclass.constructor.call(this,config); - this.userRecord = new Ext.data.Record.create(['usergroup','name','member','role','rolename','primary_group']); - this.addEvents('beforeUpdateRole','afterUpdateRole','beforeAddGroup','afterAddGroup','beforeReorderGroup','afterReorderGroup'); + MODx.grid.UserGroups.superclass.constructor.call(this, config); + this.userRecord = new Ext.data.Record.create([ + 'usergroup', + 'name', + 'member', + 'role', + 'rolename', + 'primary_group' + + ]); + this.addEvents( + 'beforeUpdateRole', + 'afterUpdateRole', + 'beforeAddGroup', + 'afterAddGroup', + 'beforeReorderGroup', + 'afterReorderGroup' + ); }; -Ext.extend(MODx.grid.UserGroups,MODx.grid.LocalGrid,{ - _showMenu: function(g,ri,e) { +Ext.extend(MODx.grid.UserGroups, MODx.grid.LocalGrid, { + _showMenu: function(grid, rowIndex, e) { e.stopEvent(); e.preventDefault(); - var m = this.menu; - m.recordIndex = ri; - m.record = this.getStore().getAt(ri).data; - if (!this.getSelectionModel().isSelected(ri)) { - this.getSelectionModel().selectRow(ri); + const { menu } = this; + menu.recordIndex = rowIndex; + menu.record = this.getStore().getAt(rowIndex).data; + if (!this.getSelectionModel().isSelected(rowIndex)) { + this.getSelectionModel().selectRow(rowIndex); } - m.removeAll(); - m.add({ - text: _('user_role_update') - ,handler: this.updateRole - ,scope: this - },'-',{ - text: _('user_group_user_remove') - ,handler: this.remove.createDelegate(this,[{text: _('user_group_user_remove_confirm')}]) - ,scope: this + menu.removeAll(); + menu.add({ + text: _('user_role_update'), + handler: this.updateRole, + scope: this + }, '-', { + text: _('user_group_user_remove'), + handler: this.remove.createDelegate(this, [{ + text: _('user_group_user_remove_confirm') + }]), + scope: this }); - m.showAt(e.xy); - } + menu.showAt(e.xy); + }, - ,onBeforeRowMove: function(dt,sri,ri,sels) { - if (!this.fireEvent('beforeReorderGroup',{dt:dt,sri:sri,ri:ri,sels:sels})) { + /** + * @deprecated Appears to be unused (including the beforeReorderGroup event) + */ + onBeforeRowMove: function(dropTarget, fromRowIndex, toRowIndex, selections) { + if (!this.fireEvent('beforeReorderGroup', { + dt: dropTarget, + sri: fromRowIndex, + ri: toRowIndex, + sels: selections + })) { return false; } return true; - } + }, - ,onAfterRowMove: function(dt,sri,ri,sels) { - var s = this.getStore(); - var sourceRec = s.getAt(sri); - var belowRec = s.getAt(ri); - var total = s.getTotalCount(); + onAfterRowMove: function(dropTarget, fromRowIndex, toRowIndex, selections) { + const + store = this.getStore(), + firstDraggedRecord = store.getAt(fromRowIndex), + total = store.getTotalCount() + ; + firstDraggedRecord.set('rank', fromRowIndex); + firstDraggedRecord.commit(); - sourceRec.set('rank',sri); - sourceRec.commit(); - - /* get all rows below ri, and up their rank by 1 */ - var brec; - for (var x=(ri-1);x Date: Mon, 18 Nov 2024 22:07:46 -0500 Subject: [PATCH 29/39] User Access updates Updates display of and ability to select row actions (gear icon). Also adjustments made to base grid class. --- core/lexicon/en/user.inc.php | 4 + .../Processors/Security/Group/GetList.php | 30 +++ .../Processors/Security/User/Get.php | 3 + .../assets/modext/widgets/core/modx.grid.js | 220 ++++++++++++------ .../widgets/security/modx.grid.user.group.js | 113 +++++---- 5 files changed, 256 insertions(+), 114 deletions(-) diff --git a/core/lexicon/en/user.inc.php b/core/lexicon/en/user.inc.php index c9904102c6..92cfec9fff 100644 --- a/core/lexicon/en/user.inc.php +++ b/core/lexicon/en/user.inc.php @@ -1,4 +1,5 @@ Set up your password

          We received a request to set up your MODX Revolution password. You can set up your password by clicking the button below and following the instructions on screen.

          Set up my password

          If you did not send this request, please ignore this email.

          '; + +// Aliases +$_lang['user_group_user_create'] = $_lang['user_group_user_add']; diff --git a/core/src/Revolution/Processors/Security/Group/GetList.php b/core/src/Revolution/Processors/Security/Group/GetList.php index cb2e4e7f3b..09c8f4dfd5 100644 --- a/core/src/Revolution/Processors/Security/Group/GetList.php +++ b/core/src/Revolution/Processors/Security/Group/GetList.php @@ -1,4 +1,5 @@ false, 'combo' => false, ]); + $this->canEditGroups = $this->modx->hasPermission('usergroup_edit'); + $this->canEditRoles = $this->modx->hasPermission('edit_role'); + + $this->modx->log( + \modX::LOG_LEVEL_ERROR, + "\r\t initialize: + \t\t\$canEditGroups: {$this->canEditGroups}" + ); return $initialized; } @@ -111,4 +124,21 @@ public function prepareQueryBeforeCount(xPDOQuery $c) return $c; } + + /** + * @param xPDOObject $object + * @return array + */ + public function prepareRow(xPDOObject $object) + { + $userGroupData = $object->toArray('', false, true); + $userGroupData['canEditGroups'] = $this->canEditGroups; + $userGroupData['canEditRoles'] = $this->canEditRoles; + $this->modx->log( + \modX::LOG_LEVEL_ERROR, + "\r\t prepareRow: + \t\t\$userGroupData: " . print_r($userGroupData, true) + ); + return $userGroupData; + } } diff --git a/core/src/Revolution/Processors/Security/User/Get.php b/core/src/Revolution/Processors/Security/User/Get.php index fa746b8ea9..fbca83c317 100644 --- a/core/src/Revolution/Processors/Security/User/Get.php +++ b/core/src/Revolution/Processors/Security/User/Get.php @@ -75,6 +75,9 @@ public function getGroups() $this->object->get('primary_group') === $member->get('user_group'), $member->get('rank'), $member->get('user_group_desc'), + $this->modx->hasPermission('usergroup_edit'), + $this->modx->hasPermission('usergroup_user_edit'), + $this->modx->hasPermission('edit_role') ]; } $this->object->set('groups', '(' . $this->modx->toJSON($data) . ')'); diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index 37cb696704..ef53187e58 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -371,9 +371,21 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { excluding create/new permissions (since that is not executed by our context/actions menus). */ if (this.showActionsMenu) { - const { isProtected } = record.json; // Export is always available; only continue filtering if grid does not offer export if (!this.gridMenuActions.includes('export')) { + /** + * @var {Object} permissionsDataSource Specifies the property where the record's + * permissions can be found. Local grids use Array stores where only the data *values* + * are stored in a simple array (record.json); the permissions and other object data must + * be stored in record.data. Remote stores, however, store their non-form (derived) data + * such as permissions in record.json. + */ + const + permissionsDataSource = this instanceof MODx.grid.LocalGrid && !(this instanceof MODx.grid.JsonGrid) + ? record.data + : record.json, + isProtected = permissionsDataSource?.isProtected || false + ; if (!this.userHasSavePermissions && isProtected) { return; } @@ -383,10 +395,10 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return; } } - if (Object.hasOwn(record.json, 'permissions')) { + if (Object.hasOwn(permissionsDataSource, 'permissions')) { if ( - Ext.isEmpty(record.json.permissions) - || Object.values(record.json.permissions).every(permission => !permission) + Ext.isEmpty(permissionsDataSource.permissions) + || Object.values(permissionsDataSource.permissions).every(permission => !permission) ) { return; } @@ -412,8 +424,14 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return []; }, - actionContextMenu: function(record, recordIndex, e) { - this._showMenu(this, recordIndex, e); + /** + * Builds the menu activated by clicking an action column icon (typically gear menu) + * @param {*} record The selected row's record + * @param {*} rowIndex The selected row's zero-based index + * @param {Ext.EventObjectImpl} e The Ext extended event object + */ + actionContextMenu: function(record, rowIndex, e) { + this._showMenu(this, rowIndex, e); }, addContextMenuItem: function(items) { @@ -470,40 +488,52 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { }, /** - * @property {Function} setShowActionsMenu - Based on properties set in the calling child class and the + * Based on properties set in the calling child class and the * the current user's permissions for actions taken within that class (create, edit, delete, etc), * evaluates whether the actions menu trigger should appear and sets boolean value on the showActionsMenu property + * @param {Array} permissions Optional custom list of permissions required to show actions * * @return void */ - setShowActionsMenu: function() { + setShowActionsMenu: function(permissions = []) { if (this.config.disableContextMenuAction === true) { this.showActionsMenu = false; return; } - const permissionsValues = []; - this.gridMenuActions.forEach(mode => { - mode = mode === 'duplicate' ? 'userCanCreate' : `userCan${Ext.util.Format.capitalize(mode)}`; - const modePermission = mode === 'userCanExport' ? true : this[mode]; - if (['userCanCreate', 'userCanEdit'].includes(mode) && modePermission === true) { - this.userHasSavePermissions = true; - } - permissionsValues.push(modePermission); - }); - this.showActionsMenu = !(permissionsValues.length === 0 || permissionsValues.every(value => value === false) === true); + if (permissions.length > 0) { + this.showActionsMenu = this.setUserHasPermissions(null, permissions, false); + } else { + const permissionsValues = []; + this.gridMenuActions.forEach(mode => { + mode = mode === 'duplicate' ? 'userCanCreate' : `userCan${Ext.util.Format.capitalize(mode)}`; + const modePermission = mode === 'userCanExport' ? true : this[mode]; + if (['userCanCreate', 'userCanEdit'].includes(mode) && modePermission === true) { + this.userHasSavePermissions = true; + } + permissionsValues.push(modePermission); + }); + this.showActionsMenu = !(permissionsValues.length === 0 || permissionsValues.every(value => value === false) === true); + } }, - _showMenu: function(g, ri, e) { + /** + * Displays a row's context menu + * @param {Object} grid The selected row's grid + * @param {Number} rowIndex The selected row's zero-based index + * @param {Ext.EventObjectImpl} e The Ext extended event object + */ + _showMenu: function(grid, rowIndex, e) { e.stopEvent(); e.preventDefault(); - this.menu.record = this.getStore().getAt(ri).data; - if (!this.getSelectionModel().isSelected(ri)) { - this.getSelectionModel().selectRow(ri); + this.menu.record = this.getStore().getAt(rowIndex).data; + this.menu.recordIndex = rowIndex; + if (!this.getSelectionModel().isSelected(rowIndex)) { + this.getSelectionModel().selectRow(rowIndex); } this.menu.removeAll(); let menu; if (this.getMenu) { - menu = this.getMenu(g, ri, e); + menu = this.getMenu(grid, rowIndex, e); if (menu && menu.length && menu.length > 0) { this.addContextMenuItem(menu); } @@ -601,59 +631,75 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { } }, - // -> User Group-Level Permissions Checks for the calling "class" object + // -> User- /User Group-Level Permissions Checks for the calling "class" object /** - * @property {Function} setUserCanEdit - Assigns a value to userCanEdit property based on - * the user's permissions; used to adjust which menu items are available, whether to render links + * Assesses whether user can take the given action on an object or + * has been granted one of a custom list of permissions + * + * @param {String} action Identifies the action (create, edit, or delete) + * being evaluated. This applies to only a single object type and not to grids + * that have mixed object types displayed (in which case a custom list of permissions + * should be supplied to setShowActionsMenu, which in turn calls this method). + * @param {Array} permissions The list of permissions keys to be evaluated + * @returns {Boolean} Whether the user has permissions for this action or a set of custom set of permissions. + */ + setUserHasPermissions: function(action, permissions) { + const + permissionsList = permissions.map(item => item.trim()), + hasPermissions = action + ? permissionsList.every(permission => MODx.perm[permission]) + : permissionsList.some(permission => MODx.perm[permission]) + ; + if (action) { + this[`userCan${Ext.util.Format.capitalize(action)}`] = hasPermissions; + } + // Conditional needed, as we only want to change userHasPermissions if true + if (hasPermissions) { + this.userHasPermissions = true; + } + return hasPermissions; + }, + + /** + * Assigns a value to userCanEdit property based on the user's permissions; + * used to adjust which menu items are available, whether to render links * to and item's editing page, and css cues across many grid classes * - * @param {Array} groupPermissions - A set of permissions keys to evaluate; note that many areas currently + * @param {Array} permissions - A set of permissions keys to evaluate; note that many areas currently * rely on a pair of permissions (save_x and edit_x), both of which must be enabled to edit a grid item * * @return void */ - setUserCanEdit: function(groupPermissions) { - groupPermissions = groupPermissions.map(item => item.trim()); - this.userCanEdit = groupPermissions.every(permission => MODx.perm[permission]); - if (this.userCanEdit) { - this.userHasPermissions = true; - } + setUserCanEdit: function(permissions) { + this.setUserHasPermissions('edit', permissions); }, /** - * @property {Function} setUserCanCreate - Assigns a value to userCanCreate property based on - * the user's permissions; used to adjust which menu items are available (namely the Duplicate item) + * Assigns a value to userCanCreate property based on the user's permissions; + * used to adjust which menu items are available (namely the Duplicate item) * and whether to render the Create button in the grid's toolbar * - * @param {Array} groupPermissions - A set of permissions keys to evaluate; note that many areas currently + * @param {Array} permissions - A set of permissions keys to evaluate; note that many areas currently * rely on a pair of permissions (save_x and new_x), both of which must be enabled to create/duplicate a grid item * * @return void */ - setUserCanCreate: function(groupPermissions) { - groupPermissions = groupPermissions.map(item => item.trim()); - this.userCanCreate = groupPermissions.every(permission => MODx.perm[permission]); - if (this.userCanCreate) { - this.userHasPermissions = true; - } + setUserCanCreate: function(permissions) { + this.setUserHasPermissions('create', permissions); }, /** - * @property {Function} setUserCanDelete - Assigns a value to userCanDelete property based on - * the user's permissions; used to adjust which menu items are available in the context menus + * Assigns a value to userCanDelete property based on the user's permissions; + * used to adjust which menu items are available in the context menus * and whether to render the Delete menu item within a grid toolbar's Batch button * - * @param {Array} groupPermissions - A set of permissions keys to evaluate + * @param {Array} permissions - A set of permissions keys to evaluate * * @return void */ - setUserCanDelete: function(groupPermissions) { - groupPermissions = groupPermissions.map(item => item.trim()); - this.userCanDelete = groupPermissions.every(permission => MODx.perm[permission]); - if (this.userCanDelete) { - this.userHasPermissions = true; - } + setUserCanDelete: function(permissions) { + this.setUserHasPermissions('delete', permissions); }, // -> Record-Level Permissions Checks, for objects with specific policies @@ -1130,7 +1176,40 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { }, /** - * Builds the bulk actions button, containing a menu of various actions + * Builds the top toolbar's create/add button + * @param {String} objectType Identifier for object being worked with + * @param {String|Object} createHandler The name of the handler method or an object containing + * a custom configuration (typically a form window config used to create a new record) + * @param {*} createPermission Name of the grid property that specifies whether the + * current user has necessary permissions to create new records + * @returns {Object} An Ext button config object + */ + getCreateButton: function(objectType, createHandler = 'create', createPermission = 'userCanCreate') { + const + handler = typeof createHandler === 'string' + ? this[createHandler] + : createHandler, + text = _(`${objectType.toLowerCase()}_create`) || _('create') + ; + return { + text: text, + cls: 'primary-button', + handler: handler, + listeners: { + render: { + fn: function(btn) { + if (!this[createPermission]) { + btn.hide(); + } + }, + scope: this + } + } + }; + }, + + /** + * Builds the top toolbar's bulk actions button, containing a menu of various actions * (typically only contains a delete action) * @param {String} objectType Identifier for object being worked with * @param {String} deleteAction Processor path for the removal action @@ -1471,23 +1550,25 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { if (this.destroying) { return MODx.grid.Grid.superclass.remove.apply(this, arguments); } - const r = this.menu.record; + const + { record } = this.menu, + { saveParams } = this.config || {}, + { primaryKey } = this.config || 'id' + ; text = text || 'confirm_remove'; - const p = this.config.saveParams || {}; - Ext.apply(p, { action: action || 'remove' }); - const k = this.config.primaryKey || 'id'; - p[k] = r[k]; + Ext.apply(saveParams, { action: action || 'remove' }); + saveParams[primaryKey] = record[primaryKey]; - if (this.fireEvent('beforeRemoveRow', r)) { + if (this.fireEvent('beforeRemoveRow', record)) { MODx.msg.confirm({ title: _('warning'), - text: _(text, r), + text: _(text, record), url: this.config.url, - params: p, + params: saveParams, listeners: { success: { fn: function() { - this.removeActiveRow(r); + this.removeActiveRow(record); }, scope: this } @@ -1496,10 +1577,10 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { } }, - removeActiveRow: function(r) { - if (this.fireEvent('afterRemoveRow', r)) { - const rx = this.getSelectionModel().getSelected(); - this.getStore().remove(rx); + removeActiveRow: function(record) { + if (this.fireEvent('afterRemoveRow', record)) { + const selection = this.getSelectionModel().getSelected(); + this.getStore().remove(selection); } }, @@ -1530,7 +1611,6 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { recordIndex = this.store.indexOf(record) ; this.menu.record = record.data; - this[actionHandler](record, recordIndex, e); }, @@ -1871,7 +1951,6 @@ MODx.grid.LocalGrid = function(config = {}) { Ext.applyIf(config, { title: '', enableColumnMove: true, - //* * NEW groupingConfig: { hideGroupedColumn: config.hideGroupedColumn } @@ -2442,12 +2521,12 @@ MODx.grid.JsonGrid = function(config = {}) { }; Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { getMenu: function() { - const m = []; - m.push({ + const menu = []; + menu.push({ text: _('remove'), handler: this.removeElement }); - return m; + return menu; }, /** @@ -2500,6 +2579,7 @@ Ext.extend(MODx.grid.JsonGrid, MODx.grid.LocalGrid, { }, renderListener: function(grid) { + // eslint-disable-next-line no-new new Ext.dd.DropTarget(grid.container, { copy: false, ddGroup: `${this.ident}-json-grid-dd`, diff --git a/manager/assets/modext/widgets/security/modx.grid.user.group.js b/manager/assets/modext/widgets/security/modx.grid.user.group.js index 630b8d9d70..7b4cec3ccd 100644 --- a/manager/assets/modext/widgets/security/modx.grid.user.group.js +++ b/manager/assets/modext/widgets/security/modx.grid.user.group.js @@ -15,6 +15,11 @@ MODx.grid.UserGroups = function(config = {}) { Ext.applyIf(config, { title: '', id: 'modx-grid-user-groups', + /* + url and baseParams are not utilized by the core when this + grid is used (only in User > Access Permissions). Should remove + if this class is not meant to be somehow used externally (via Extra) + */ url: MODx.config.connector_url, baseParams: { action: 'Security/Group/GetList' @@ -27,7 +32,10 @@ MODx.grid.UserGroups = function(config = {}) { 'rolename', 'primary_group', 'rank', - 'user_group_desc' + 'user_group_desc', + 'canEditGroups', + 'canEditGroupUsers', + 'canEditRoles' ], cls: 'modx-grid modx-grid-draggable', columns: [ @@ -38,10 +46,13 @@ MODx.grid.UserGroups = function(config = {}) { width: 175, renderer: { fn: function(value, metaData, record) { - return this.renderLink(value, { - href: `?a=security/usergroup/update&id=${record.data.usergroup}`, - target: '_blank' - }); + return record.data.canEditGroups + ? this.renderLink(value, { + href: `?a=security/usergroup/update&id=${record.data.usergroup}`, + target: '_blank' + }) + : value + ; }, scope: this } @@ -51,10 +62,13 @@ MODx.grid.UserGroups = function(config = {}) { width: 175, renderer: { fn: function(value, metaData, record) { - return this.renderLink(value, { - href: '?a=security/permission', - target: '_blank' - }); + return record.data.canEditRoles + ? this.renderLink(value, { + href: `?a=security/permission&tab=1&role=${record.data.role}`, + target: '_blank' + }) + : value + ; }, scope: this } @@ -69,7 +83,17 @@ MODx.grid.UserGroups = function(config = {}) { } } ], - plugins: [ + plugins: [this.exp], + tbar: [ + this.getCreateButton('user_group_user', 'addGroup', 'userCanEditGroupUsers') + ] + }); + + this.userCanEditGroups = MODx.perm.usergroup_edit; + this.userCanEditGroupUsers = MODx.perm.usergroup_user_edit; + + if (this.userCanEditGroupUsers) { + config.plugins.push( new Ext.ux.dd.GridDragDropRowOrder({ copy: false, scrollable: true, @@ -80,23 +104,19 @@ MODx.grid.UserGroups = function(config = {}) { scope: this }, /** - * @deprecated Appears to be unused + * @deprecated In 3.1, appears to be unused */ beforerowmove: { fn: this.onBeforeRowMove, scope: this } } - }), - this.exp - ], - tbar: [{ - text: _('user_group_user_add'), - cls: 'primary-button', - handler: this.addGroup - }] - }); + }) + ); + } + MODx.grid.UserGroups.superclass.constructor.call(this, config); + this.userRecord = new Ext.data.Record.create([ 'usergroup', 'name', @@ -104,7 +124,6 @@ MODx.grid.UserGroups = function(config = {}) { 'role', 'rolename', 'primary_group' - ]); this.addEvents( 'beforeUpdateRole', @@ -114,34 +133,41 @@ MODx.grid.UserGroups = function(config = {}) { 'beforeReorderGroup', 'afterReorderGroup' ); + + /** + * Implementing alternate usage for applying grid permissions, as this grid + * displays data and assigns values from/to different object types + * (User, User Groups, Roles) + */ + this.setShowActionsMenu(['usergroup_edit', 'usergroup_user_edit']); }; Ext.extend(MODx.grid.UserGroups, MODx.grid.LocalGrid, { - _showMenu: function(grid, rowIndex, e) { - e.stopEvent(); - e.preventDefault(); - const { menu } = this; - menu.recordIndex = rowIndex; - menu.record = this.getStore().getAt(rowIndex).data; - if (!this.getSelectionModel().isSelected(rowIndex)) { - this.getSelectionModel().selectRow(rowIndex); + getMenu: function() { + const menu = []; + if (this.userCanEditGroupUsers) { + menu.push({ + text: _('user_role_update'), + handler: this.updateRole, + scope: this + }); } - menu.removeAll(); - menu.add({ - text: _('user_role_update'), - handler: this.updateRole, - scope: this - }, '-', { - text: _('user_group_user_remove'), - handler: this.remove.createDelegate(this, [{ - text: _('user_group_user_remove_confirm') - }]), - scope: this - }); - menu.showAt(e.xy); + if (this.userCanEditGroups) { + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('user_group_user_remove'), + handler: this.remove.createDelegate(this, [{ + text: _('user_group_user_remove_confirm') + }]), + scope: this + }); + } + return menu; }, /** - * @deprecated Appears to be unused (including the beforeReorderGroup event) + * @deprecated In 3.1, appears to be unused (including the beforeReorderGroup event) */ onBeforeRowMove: function(dropTarget, fromRowIndex, toRowIndex, selections) { if (!this.fireEvent('beforeReorderGroup', { @@ -218,7 +244,6 @@ Ext.extend(MODx.grid.UserGroups, MODx.grid.LocalGrid, { newRecord = new this.userRecord(response) ; store.add(newRecord); - this.fireEvent('afterAddGroup', response); }, scope: this From 1eec6dc5179633a34115cada626464be9a3212c2 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 18 Nov 2024 22:58:40 -0500 Subject: [PATCH 30/39] Update GetList.php Remove dev logging --- .../Revolution/Processors/Security/Group/GetList.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/core/src/Revolution/Processors/Security/Group/GetList.php b/core/src/Revolution/Processors/Security/Group/GetList.php index 09c8f4dfd5..4405ec8688 100644 --- a/core/src/Revolution/Processors/Security/Group/GetList.php +++ b/core/src/Revolution/Processors/Security/Group/GetList.php @@ -47,12 +47,6 @@ public function initialize() ]); $this->canEditGroups = $this->modx->hasPermission('usergroup_edit'); $this->canEditRoles = $this->modx->hasPermission('edit_role'); - - $this->modx->log( - \modX::LOG_LEVEL_ERROR, - "\r\t initialize: - \t\t\$canEditGroups: {$this->canEditGroups}" - ); return $initialized; } @@ -134,11 +128,7 @@ public function prepareRow(xPDOObject $object) $userGroupData = $object->toArray('', false, true); $userGroupData['canEditGroups'] = $this->canEditGroups; $userGroupData['canEditRoles'] = $this->canEditRoles; - $this->modx->log( - \modX::LOG_LEVEL_ERROR, - "\r\t prepareRow: - \t\t\$userGroupData: " . print_r($userGroupData, true) - ); + return $userGroupData; } } From 076e42e68a5a1729414c2d12cefdfd3f6d2e4e99 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Tue, 19 Nov 2024 00:33:21 -0500 Subject: [PATCH 31/39] Update modx.grid.content.type.js Formatting, code style changes only --- .../widgets/system/modx.grid.content.type.js | 591 +++++++++--------- 1 file changed, 295 insertions(+), 296 deletions(-) diff --git a/manager/assets/modext/widgets/system/modx.grid.content.type.js b/manager/assets/modext/widgets/system/modx.grid.content.type.js index 16bda8b568..054656e755 100644 --- a/manager/assets/modext/widgets/system/modx.grid.content.type.js +++ b/manager/assets/modext/widgets/system/modx.grid.content.type.js @@ -4,35 +4,37 @@ * @param {Object} config An object of options. * @xtype modx-panel-content-type */ -MODx.panel.ContentType = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-content-type' - ,cls: 'container' - ,url: MODx.config.connector_url - ,defaults: { collapsible: false ,autoHeight: true } - ,items: [{ - html: _('content_types') - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('content_types') - ,layout: 'form' - ,itemId: 'form' - ,items: [{ - html: '

          '+_('content_type_desc')+'

          ' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-content-type' - ,itemId: 'grid' - ,cls:'main-wrapper' - ,preventRender: true +MODx.panel.ContentType = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-content-type', + cls: 'container', + url: MODx.config.connector_url, + defaults: { + collapsible: false, + autoHeight: true + }, + items: [{ + html: _('content_types'), + xtype: 'modx-header' + }, MODx.getPageStructure([{ + title: _('content_types'), + layout: 'form', + itemId: 'form', + items: [{ + html: `

          ${_('content_type_desc')}

          `, + xtype: 'modx-description' + }, { + xtype: 'modx-grid-content-type', + itemId: 'grid', + cls: 'main-wrapper', + preventRender: true }] }])] }); - MODx.panel.ContentType.superclass.constructor.call(this,config); + MODx.panel.ContentType.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.ContentType,MODx.FormPanel,{}); -Ext.reg('modx-panel-content-type',MODx.panel.ContentType); +Ext.extend(MODx.panel.ContentType, MODx.FormPanel, {}); +Ext.reg('modx-panel-content-type', MODx.panel.ContentType); /** * Loads a grid of content types @@ -42,115 +44,122 @@ Ext.reg('modx-panel-content-type',MODx.panel.ContentType); * @param {Object} config An object of options. * @xtype modx-grid-contenttype */ -MODx.grid.ContentType = function(config) { - config = config || {}; - var binaryColumn = new Ext.ux.grid.CheckColumn({ - header: _('binary') - ,dataIndex: 'binary' - ,width: 40 - ,sortable: true +MODx.grid.ContentType = function(config = {}) { + const binaryColumn = new Ext.ux.grid.CheckColumn({ + header: _('binary'), + dataIndex: 'binary', + width: 40, + sortable: true }); - - Ext.applyIf(config,{ - url: MODx.config.connector_url - ,baseParams: { + Ext.applyIf(config, { + url: MODx.config.connector_url, + baseParams: { action: 'System/ContentType/GetList' - } - ,autosave: true - ,save_action: 'System/ContentType/UpdateFromGrid' - ,fields: ['id','name','mime_type','file_extensions','icon','headers','binary','description'] - ,paging: true - ,remoteSort: true - ,plugins: binaryColumn - ,columns: [{ - header: _('id') - ,dataIndex: 'id' - ,width: 50 - ,sortable: true - },{ - header: _('name') - ,dataIndex: 'name' - ,sortable: true - ,editor: { xtype: 'textfield' } - },{ - header: _('description') - ,dataIndex: 'description' - ,editor: { xtype: 'textfield' } - ,width: 200 - },{ - header: _('mime_type') - ,dataIndex: 'mime_type' - ,sortable: true - ,editor: { xtype: 'textfield' } - ,width: 80 - },{ - header: _('file_extensions') - ,dataIndex: 'file_extensions' - ,sortable: true - ,editor: { xtype: 'textfield' } - },{ - header: _('icon') - ,dataIndex: 'icon' - ,sortable: false - ,editor: { xtype: 'textfield' } - ,renderer: this.renderIconField.createDelegate(this,[this],true) + }, + autosave: true, + save_action: 'System/ContentType/UpdateFromGrid', + fields: [ + 'id', + 'name', + 'mime_type', + 'file_extensions', + 'icon', + 'headers', + 'binary', + 'description' + ], + paging: true, + remoteSort: true, + plugins: binaryColumn, + columns: [{ + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('name'), + dataIndex: 'name', + sortable: true, + editor: { xtype: 'textfield' } + }, { + header: _('description'), + dataIndex: 'description', + editor: { xtype: 'textfield' }, + width: 200 + }, { + header: _('mime_type'), + dataIndex: 'mime_type', + sortable: true, + editor: { xtype: 'textfield' }, + width: 80 + }, { + header: _('file_extensions'), + dataIndex: 'file_extensions', + sortable: true, + editor: { xtype: 'textfield' } + }, { + header: _('icon'), + dataIndex: 'icon', + sortable: false, + editor: { xtype: 'textfield' }, + renderer: this.renderIconField.createDelegate(this, [this], true) }, binaryColumn, { - dataIndex: 'headers' - ,hidden: true - }] - ,tbar: [{ - text: _('create') - ,cls: 'primary-button' - ,handler: this.newContentType - ,scope: this + dataIndex: 'headers', + hidden: true + }], + tbar: [{ + text: _('create'), + cls: 'primary-button', + handler: this.newContentType, + scope: this }] }); - MODx.grid.ContentType.superclass.constructor.call(this,config); + MODx.grid.ContentType.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.ContentType,MODx.grid.Grid,{ +Ext.extend(MODx.grid.ContentType, MODx.grid.Grid, { getMenu: function() { - var m = []; - m.push({ - text: _('edit') - ,handler: function(btn, e) { - var window = new MODx.window.CreateContentType({ - record: this.menu.record - ,title: _('edit') - ,action: 'System/ContentType/Update' - ,listeners: { + const menu = []; + menu.push({ + text: _('edit'), + handler: function(btn, e) { + const window = new MODx.window.CreateContentType({ + record: this.menu.record, + title: _('edit'), + action: 'System/ContentType/Update', + listeners: { success: { - fn: this.refresh - ,scope: this + fn: this.refresh, + scope: this } } }); window.setRecord(this.menu.record); window.show(e.target); - } - ,scope: this + }, + scope: this }); - m.push({ - text: _('delete') - ,handler: this.confirm.createDelegate(this,['System/ContentType/Remove',_('content_type_remove_confirm')]) + menu.push({ + text: _('delete'), + handler: this.confirm.createDelegate(this, ['System/ContentType/Remove', _('content_type_remove_confirm')]) }); - return m; - } + return menu; + }, - ,newContentType: function(btn, e) { - var window = new MODx.window.CreateContentType({ + newContentType: function(btn, e) { + const window = new MODx.window.CreateContentType({ listeners: { success: { - fn: this.refresh - ,scope: this + fn: this.refresh, + scope: this } } }); window.show(e.target); - } + }, - ,renderIconField: function (v, md, rec) { - return new Ext.XTemplate('   {icon:htmlEncode}').apply(rec.data); + renderIconField: function(value, metaData, record) { + return new Ext.XTemplate('   {icon:htmlEncode}').apply(record.data); } }); Ext.reg('modx-grid-content-type', MODx.grid.ContentType); @@ -163,134 +172,124 @@ Ext.reg('modx-grid-content-type', MODx.grid.ContentType); * @param {Object} config An object of options. * @xtype modx-window-content-type-create */ -MODx.window.CreateContentType = function(config) { - config = config || {}; - this.ident = config.ident || 'modx-cct'+Ext.id(); - Ext.applyIf(config,{ - title: _('create') - ,width: 600 - ,url: MODx.config.connector_url - ,action: 'System/ContentType/Create' - ,bwrapCssClass: 'x-window-with-tabs' - ,fields: [{ - xtype: 'modx-tabs' - ,items: [{ - title: _('content_type_main_tab') - ,layout: 'form' - ,items: [{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,anchor: '100%' - ,border: false - } - ,items: [{ - columnWidth: .6 - ,defaults: { - msgTarget: 'under' - } - ,items: [{ - xtype: 'hidden' - ,name: 'id' - },{ - fieldLabel: _('name') - ,name: 'name' - ,id: this.ident+'-name' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: false - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: this.ident+'-name' - ,html: _('name_desc') - ,cls: 'desc-under' - },{ - fieldLabel: _('mime_type') - ,description: MODx.expandHelp ? '' : _('mime_type_desc') - ,name: 'mime_type' - ,id: this.ident+'-mime-type' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: false - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: this.ident+'-mime-type' - ,html: _('mime_type_desc') - ,cls: 'desc-under' +MODx.window.CreateContentType = function(config = {}) { + this.ident = config.ident || `modx-cct${Ext.id()}`; + Ext.applyIf(config, { + title: _('create'), + width: 600, + url: MODx.config.connector_url, + action: 'System/ContentType/Create', + bwrapCssClass: 'x-window-with-tabs', + fields: [{ + xtype: 'modx-tabs', + items: [{ + title: _('content_type_main_tab'), + layout: 'form', + items: [{ + layout: 'column', + border: false, + defaults: { + layout: 'form', + labelSeparator: '' + }, + items: [{ + columnWidth: 0.6, + defaults: { + msgTarget: 'under', + anchor: '100%', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'hidden', + name: 'id' + }, { + fieldLabel: _('name'), + name: 'name', + xtype: 'textfield', + allowBlank: false + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('name_desc'), + cls: 'desc-under' + }, { + fieldLabel: _('mime_type'), + description: MODx.expandHelp ? '' : _('mime_type_desc'), + name: 'mime_type', + xtype: 'textfield', + allowBlank: false + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mime_type_desc'), + cls: 'desc-under' }] - },{ - columnWidth: .4 - ,defaults: { - msgTarget: 'under' - } - ,items: [{ - fieldLabel: _('icon') - ,description: MODx.expandHelp ? '' : _('icon_desc') - ,name: 'icon' - ,id: this.ident+'-icon' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: true - },{ - fieldLabel: _('file_extensions') - ,description: MODx.expandHelp ? '' : _('file_extensions_desc') - ,name: 'file_extensions' - ,id: this.ident+'-file-extensions' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: this.ident+'-file-extensions' - ,html: _('file_extensions_desc') - ,cls: 'desc-under' + }, { + columnWidth: 0.4, + defaults: { + msgTarget: 'under', + anchor: '100%', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + fieldLabel: _('icon'), + description: MODx.expandHelp ? '' : _('icon_desc'), + name: 'icon', + xtype: 'textfield' + }, { + fieldLabel: _('file_extensions'), + description: MODx.expandHelp ? '' : _('file_extensions_desc'), + name: 'file_extensions', + xtype: 'textfield' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('file_extensions_desc'), + cls: 'desc-under' }] }] - },{ - xtype: 'xcheckbox' - ,hideLabel: true - ,boxLabel: _('binary_desc') - ,name: 'binary' - ,hiddenName: 'binary' - ,id: this.ident+'-binary' - ,anchor: '100%' - },{ - fieldLabel: _('description') - ,name: 'description' - ,id: 'modx-'+this.ident+'-description' - ,xtype: 'textarea' - ,anchor: '100%' - ,grow: true - },{ - xtype: 'hidden' - ,name: 'headers' + }, { + xtype: 'xcheckbox', + hideLabel: true, + boxLabel: _('binary_desc'), + name: 'binary', + hiddenName: 'binary' + }, { + fieldLabel: _('description'), + name: 'description', + xtype: 'textarea', + anchor: '100%', + grow: true + }, { + xtype: 'hidden', + name: 'headers' }] - },{ - title: _('content_type_header_tab') - ,layout: 'anchor' - ,anchor: '100%' - ,items: [{ - xtype: 'modx-content-type-headers-grid' - ,id: 'headers' + }, { + title: _('content_type_header_tab'), + layout: 'anchor', + anchor: '100%', + items: [{ + xtype: 'modx-content-type-headers-grid', + id: 'headers' }] }] - }] - ,keys: [] + }], + keys: [] }); MODx.window.CreateContentType.superclass.constructor.call(this, config); this.on('beforeSubmit', this.beforeSubmit, this); }; -Ext.extend(MODx.window.CreateContentType,MODx.Window, { +Ext.extend(MODx.window.CreateContentType, MODx.Window, { setRecord: function(record) { this.setValues(record); - - var grid = Ext.getCmp('headers') - ,store = grid.getStore(); - + const + grid = Ext.getCmp('headers'), + store = grid.getStore() + ; store.removeAll(); if (record.headers && record.headers.length > 0) { Ext.each(record.headers, function(header) { @@ -299,15 +298,16 @@ Ext.extend(MODx.window.CreateContentType,MODx.Window, { })); }, this); } - } + }, - ,beforeSubmit: function(o) { - var grid = Ext.getCmp('headers'), - store = grid.getStore() - ,records = store.getRange() - ,form = this.fp.getForm(); - - var results = []; + beforeSubmit: function(o) { + const + grid = Ext.getCmp('headers'), + store = grid.getStore(), + records = store.getRange(), + form = this.fp.getForm(), + results = [] + ; Ext.each(records, function(rec) { results.push(rec.get('header')); }, this); @@ -317,78 +317,78 @@ Ext.extend(MODx.window.CreateContentType,MODx.Window, { return true; } }); -Ext.reg('modx-window-content-type-create',MODx.window.CreateContentType); +Ext.reg('modx-window-content-type-create', MODx.window.CreateContentType); /** * * @param config * @constructor */ -MODx.ContentTypeHeaderGrid = function(config) { - config = config || {}; - +MODx.ContentTypeHeaderGrid = function(config = {}) { Ext.apply(config, { - fields: ['id', 'header'] - ,columns: [{ - header: _('content_type_header') - ,dataIndex: 'header' - }] - ,deferredRender: true - ,autoHeight: true - ,tbar: [{ - text: _('create') - ,cls: 'primary-button' - ,handler: this.add - ,scope: this + fields: ['id', 'header'], + columns: [{ + header: _('content_type_header'), + dataIndex: 'header' + }], + deferredRender: true, + autoHeight: true, + tbar: [{ + text: _('create'), + cls: 'primary-button', + handler: this.add, + scope: this }] }); MODx.ContentTypeHeaderGrid.superclass.constructor.call(this, config); }; Ext.extend(MODx.ContentTypeHeaderGrid, MODx.grid.LocalGrid, { - add: function(btn,e) { - var window = this.loadWindow(); + add: function(btn, e) { + const window = this.loadWindow(); window.show(e.target); - } - - ,edit: function(btn, e) { - var record = this.menu.record - ,window = this.loadWindow(record); + }, + edit: function(btn, e) { + const + { record } = this.menu, + window = this.loadWindow(record) + ; window.setValues(record); window.show(e.target); - } - - ,remove: function() { - var record = this.menu.record - ,store = this.getStore() - ,idx = store.find('header', record['header']); + }, + remove: function() { + const + { record } = this.menu, + store = this.getStore(), + idx = store.find('header', record.header) + ; store.removeAt(idx); - } + }, - ,loadWindow: function(record) { + loadWindow: function(record) { return MODx.load({ - xtype: 'modx-window-content-header' - ,grid: this - ,record: record + xtype: 'modx-window-content-header', + grid: this, + record: record }); - } + }, - ,getMenu: function() { - var m = []; - m.push({ - text: _('edit') - ,handler: this.edit - ,scope: this + getMenu: function() { + const menu = []; + menu.push({ + text: _('edit'), + handler: this.edit, + scope: this }); - m.push({ - text: _('delete') - ,handler: this.remove - ,scope: this + menu.push({ + text: _('delete'), + handler: this.remove, + scope: this }); - return m; + return menu; } }); Ext.reg('modx-content-type-headers-grid', MODx.ContentTypeHeaderGrid); @@ -398,39 +398,38 @@ Ext.reg('modx-content-type-headers-grid', MODx.ContentTypeHeaderGrid); * @param config * @constructor */ -MODx.window.ContentHeader = function(config) { - config = config || {}; - +MODx.window.ContentHeader = function(config = {}) { Ext.apply(config, { - title: _('content_type_header_title') - ,fields: [{ - xtype: 'textfield' - ,name: 'header' - ,fieldLabel: _('content_type_header') - ,anchor: '100%' - ,allowBlank: false - }] - ,closeAction: 'close' + title: _('content_type_header_title'), + fields: [{ + xtype: 'textfield', + name: 'header', + fieldLabel: _('content_type_header'), + anchor: '100%', + allowBlank: false + }], + closeAction: 'close' }); MODx.window.ContentHeader.superclass.constructor.call(this, config); }; Ext.extend(MODx.window.ContentHeader, MODx.Window, { submit: function(close) { - var values = this.fp.getForm().getValues() - ,store = this.grid.getStore(); - + const + values = this.fp.getForm().getValues(), + store = this.grid.getStore() + ; if (this.config.record && this.config.record.header) { // Existing record, let's update it - var idx = store.find('header', this.config.record.header); + const idx = store.find('header', this.config.record.header); store.removeAt(idx); store.insert(idx, new Ext.data.Record({ - header: values['header'] + header: values.header })); } else { // New record let's add it to the store store.add(new Ext.data.Record({ - header: values['header'] + header: values.header })); } From 5c414071a65559fb16f8c009b82f84973cf33da4 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 21 Nov 2024 23:01:38 -0500 Subject: [PATCH 32/39] Content Type updates Updates display of and ability to select row actions (gear icon). Also adjustments made to base grid class. --- core/lexicon/en/content_type.inc.php | 2 + .../Processors/System/ContentType/GetList.php | 49 ++++ core/src/Revolution/modContentType.php | 31 +++ .../assets/modext/widgets/core/modx.grid.js | 11 +- .../widgets/system/modx.grid.content.type.js | 243 +++++++++++------- 5 files changed, 245 insertions(+), 91 deletions(-) diff --git a/core/lexicon/en/content_type.inc.php b/core/lexicon/en/content_type.inc.php index 62063f9322..a6411fed17 100644 --- a/core/lexicon/en/content_type.inc.php +++ b/core/lexicon/en/content_type.inc.php @@ -1,4 +1,5 @@ modx->hasPermission('content_types'); + $this->canCreate = $canManage; + $this->canEdit = $canManage; + $this->canRemove = $canManage; + $this->coreContentTypes = $this->classKey::getCoreContentTypes(); + + return $initialized; + } + /** * Filter the query by the valueField of MODx.combo.ContentType to get the initially value displayed right * @param xPDOQuery $c @@ -42,4 +65,30 @@ public function prepareQueryAfterCount(xPDOQuery $c) } return $c; } + + /** + * @param xPDOObject|modContentType $object + * @return array + */ + public function prepareRow(xPDOObject $object) + { + $permissions = [ + 'create' => $this->canCreate, + 'duplicate' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ]; + $contentTypeData = $object->toArray(); + $dashboardName = $object->get('name'); + $isCoreContentType = $object->isCoreContentType($dashboardName); + + $contentTypeData['isProtected'] = $isCoreContentType; + $contentTypeData['creator'] = $isCoreContentType ? 'modx' : strtolower($this->modx->lexicon('user')) ; + if ($isCoreContentType) { + unset($permissions['delete']); + } + $contentTypeData['permissions'] = $permissions; + + return $contentTypeData; + } } diff --git a/core/src/Revolution/modContentType.php b/core/src/Revolution/modContentType.php index 4109e80c41..43ddf01e98 100644 --- a/core/src/Revolution/modContentType.php +++ b/core/src/Revolution/modContentType.php @@ -18,6 +18,17 @@ */ class modContentType extends xPDOSimpleObject { + public const CORE_TYPES = [ + 'HTML', + 'XML', + 'Text', + 'CSS', + 'JavaScript', + 'RSS', + 'JSON', + 'PDF' + ]; + /** * Returns the first extension of this Content Type. * @@ -33,4 +44,24 @@ public function getExtension() return $extension; } + + /** + * Returns a list of core Dashboards + * + * @return array + */ + public static function getCoreContentTypes(): array + { + return self::CORE_TYPES; + } + + /** + * @param string $name The name of the Dashboard + * + * @return bool + */ + public function isCoreContentType($name): bool + { + return in_array($name, static::getCoreContentTypes(), true); + } } diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index ef53187e58..a16c1fce52 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -1180,8 +1180,8 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { * @param {String} objectType Identifier for object being worked with * @param {String|Object} createHandler The name of the handler method or an object containing * a custom configuration (typically a form window config used to create a new record) - * @param {*} createPermission Name of the grid property that specifies whether the - * current user has necessary permissions to create new records + * @param {String|Boolean} createPermission Name of the grid property that specifies whether the + * current user has necessary permissions to create new records. Set to true|false to override. * @returns {Object} An Ext button config object */ getCreateButton: function(objectType, createHandler = 'create', createPermission = 'userCanCreate') { @@ -1189,7 +1189,10 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { handler = typeof createHandler === 'string' ? this[createHandler] : createHandler, - text = _(`${objectType.toLowerCase()}_create`) || _('create') + text = _(`${objectType.toLowerCase()}_create`) || _('create'), + hasPermission = typeof createPermission === 'boolean' + ? createPermission + : this[createPermission] ; return { text: text, @@ -1198,7 +1201,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { listeners: { render: { fn: function(btn) { - if (!this[createPermission]) { + if (!hasPermission) { btn.hide(); } }, diff --git a/manager/assets/modext/widgets/system/modx.grid.content.type.js b/manager/assets/modext/widgets/system/modx.grid.content.type.js index 054656e755..f7d4cc46c1 100644 --- a/manager/assets/modext/widgets/system/modx.grid.content.type.js +++ b/manager/assets/modext/widgets/system/modx.grid.content.type.js @@ -66,95 +66,143 @@ MODx.grid.ContentType = function(config = {}) { 'icon', 'headers', 'binary', - 'description' + 'description', + 'creator' ], paging: true, remoteSort: true, plugins: binaryColumn, - columns: [{ - header: _('id'), - dataIndex: 'id', - width: 50, - sortable: true - }, { - header: _('name'), - dataIndex: 'name', - sortable: true, - editor: { xtype: 'textfield' } - }, { - header: _('description'), - dataIndex: 'description', - editor: { xtype: 'textfield' }, - width: 200 - }, { - header: _('mime_type'), - dataIndex: 'mime_type', - sortable: true, - editor: { xtype: 'textfield' }, - width: 80 - }, { - header: _('file_extensions'), - dataIndex: 'file_extensions', - sortable: true, - editor: { xtype: 'textfield' } - }, { - header: _('icon'), - dataIndex: 'icon', - sortable: false, - editor: { xtype: 'textfield' }, - renderer: this.renderIconField.createDelegate(this, [this], true) - }, binaryColumn, { - dataIndex: 'headers', - hidden: true - }], - tbar: [{ - text: _('create'), - cls: 'primary-button', - handler: this.newContentType, - scope: this - }] + columns: [ + { + header: _('id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('name'), + id: 'modx-content-type--name', + dataIndex: 'name', + sortable: true, + editor: { xtype: 'textfield' }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return value; + }, + scope: this + } + }, { + header: _('description'), + id: 'modx-content-type--description', + dataIndex: 'description', + width: 200, + editor: { xtype: 'textfield' }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return value; + }, + scope: this + } + }, { + header: _('mime_type'), + id: 'modx-content-type--mime', + dataIndex: 'mime_type', + width: 80, + sortable: true, + editor: { xtype: 'textfield' }, + renderer: { + fn: function(value, metaData, record) { + // eslint-disable-next-line no-param-reassign + metaData.css = this.setEditableCellClasses(record, [record.json.isProtected]); + return value; + }, + scope: this + } + }, { + header: _('file_extensions'), + dataIndex: 'file_extensions', + sortable: true, + editor: { xtype: 'textfield' } + }, { + header: _('icon'), + dataIndex: 'icon', + sortable: false, + editor: { xtype: 'textfield' }, + renderer: this.renderIconField.createDelegate(this, [this], true) + }, + binaryColumn, + { + dataIndex: 'headers', + hidden: true + }, + this.getCreatorColumnConfig('content_types') + ], + tbar: [ + this.getCreateButton('content_types', 'newContentType') + ] }); MODx.grid.ContentType.superclass.constructor.call(this, config); + + this.gridMenuActions = ['edit', 'delete']; + + // Note there are currently no action-specific permissions for Content Types + this.setUserCanEdit(['content_types']); + this.setUserCanCreate(['content_types']); + this.setUserCanDelete(['content_types']); + this.setShowActionsMenu(); + + this.on({ + beforerender: function(grid) { + grid.view = new Ext.grid.GridView(grid.getViewConfig(false)); + }, + render: function() { + this.setEditableColumnAccess( + [ + 'modx-content-type--name', + 'modx-content-type--description', + 'modx-content-type--mime' + ] + ); + }, + beforeedit: function(e) { + const skipProtectionFieldList = ['file_extensions', 'icon']; + if ((e.record.json.isProtected && !skipProtectionFieldList.includes(e.field)) || !this.userCanEditRecord(e.record)) { + return false; + } + } + }); }; Ext.extend(MODx.grid.ContentType, MODx.grid.Grid, { getMenu: function() { - const menu = []; - menu.push({ - text: _('edit'), - handler: function(btn, e) { - const window = new MODx.window.CreateContentType({ - record: this.menu.record, - title: _('edit'), - action: 'System/ContentType/Update', - listeners: { - success: { - fn: this.refresh, - scope: this - } - } - }); - window.setRecord(this.menu.record); - window.show(e.target); - }, - scope: this - }); - menu.push({ - text: _('delete'), - handler: this.confirm.createDelegate(this, ['System/ContentType/Remove', _('content_type_remove_confirm')]) - }); - + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + if (this.userCanEdit && this.userCanEditRecord(record)) { + menu.push({ + text: _('edit'), + handler: this.updateContentType.createDelegate(this, [record], true) + }); + } + if (this.userCanDelete && this.userCanDeleteRecord(record)) { + menu.push({ + text: _('delete'), + handler: this.confirm.createDelegate(this, ['System/ContentType/Remove', _('content_type_remove_confirm')]) + }); + } return menu; }, newContentType: function(btn, e) { - const window = new MODx.window.CreateContentType({ - listeners: { - success: { - fn: this.refresh, - scope: this - } - } - }); + const window = new MODx.window.CreateContentType({ grid: this }); + window.show(e.target); + }, + + updateContentType: function(btn, e, record) { + const window = new MODx.window.UpdateContentType({ record: record, grid: this }); window.show(e.target); }, @@ -173,7 +221,6 @@ Ext.reg('modx-grid-content-type', MODx.grid.ContentType); * @xtype modx-window-content-type-create */ MODx.window.CreateContentType = function(config = {}) { - this.ident = config.ident || `modx-cct${Ext.id()}`; Ext.applyIf(config, { title: _('create'), width: 600, @@ -186,6 +233,11 @@ MODx.window.CreateContentType = function(config = {}) { title: _('content_type_main_tab'), layout: 'form', items: [{ + xtype: 'modx-description', + id: 'modx-content-type-general-desc', + hidden: !config.record.json?.isProtected, + html: _('content_type_reserved_general_desc') + }, { layout: 'column', border: false, defaults: { @@ -207,7 +259,8 @@ MODx.window.CreateContentType = function(config = {}) { fieldLabel: _('name'), name: 'name', xtype: 'textfield', - allowBlank: false + allowBlank: false, + readOnly: (config.isUpdate && config.record.json?.isProtected) || false }, { xtype: 'box', hidden: !MODx.expandHelp, @@ -218,7 +271,8 @@ MODx.window.CreateContentType = function(config = {}) { description: MODx.expandHelp ? '' : _('mime_type_desc'), name: 'mime_type', xtype: 'textfield', - allowBlank: false + allowBlank: false, + readOnly: (config.isUpdate && config.record.json?.isProtected) || false }, { xtype: 'box', hidden: !MODx.expandHelp, @@ -258,10 +312,12 @@ MODx.window.CreateContentType = function(config = {}) { hiddenName: 'binary' }, { fieldLabel: _('description'), + labelSeparator: '', name: 'description', xtype: 'textarea', anchor: '100%', - grow: true + grow: true, + readOnly: (config.isUpdate && config.record.json?.isProtected) || false }, { xtype: 'hidden', name: 'headers' @@ -280,7 +336,14 @@ MODx.window.CreateContentType = function(config = {}) { }); MODx.window.CreateContentType.superclass.constructor.call(this, config); - this.on('beforeSubmit', this.beforeSubmit, this); + this.on({ + beforeSubmit: this.beforeSubmit, + success: { + fn: function() { + this.grid.refresh(); + } + } + }); }; Ext.extend(MODx.window.CreateContentType, MODx.Window, { @@ -319,6 +382,17 @@ Ext.extend(MODx.window.CreateContentType, MODx.Window, { }); Ext.reg('modx-window-content-type-create', MODx.window.CreateContentType); +MODx.window.UpdateContentType = function(config = {}) { + Ext.applyIf(config, { + title: _('edit'), + action: 'System/ContentType/Update', + isUpdate: true + }); + MODx.window.UpdateContentType.superclass.constructor.call(this, config); + this.setRecord(config.record.data); +}; +Ext.extend(MODx.window.UpdateContentType, MODx.window.CreateContentType, {}); + /** * * @param config @@ -333,12 +407,7 @@ MODx.ContentTypeHeaderGrid = function(config = {}) { }], deferredRender: true, autoHeight: true, - tbar: [{ - text: _('create'), - cls: 'primary-button', - handler: this.add, - scope: this - }] + tbar: [this.getCreateButton('content_types', 'add', true)] }); MODx.ContentTypeHeaderGrid.superclass.constructor.call(this, config); }; From 647a7c9b00b50945c38f63862712c0cfcc5b5d65 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 23 Nov 2024 16:16:12 -0500 Subject: [PATCH 33/39] Update modx.panel.property.set.js Formatting, code style & optimization only --- .../element/modx.panel.property.set.js | 762 +++++++++--------- 1 file changed, 396 insertions(+), 366 deletions(-) diff --git a/manager/assets/modext/widgets/element/modx.panel.property.set.js b/manager/assets/modext/widgets/element/modx.panel.property.set.js index f51af014cf..f2a5023af3 100644 --- a/manager/assets/modext/widgets/element/modx.panel.property.set.js +++ b/manager/assets/modext/widgets/element/modx.panel.property.set.js @@ -4,60 +4,59 @@ * @param {Object} config An object of config properties * @xtype modx-panel-property-sets */ -MODx.panel.PropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-property-sets' - ,cls: 'container' - ,items: [{ - html: _('propertysets') - ,xtype: 'modx-header' - },MODx.getPageStructure([{ - title: _('propertysets') - ,layout: 'form' - ,id: 'modx-property-set-form' - ,border: true - ,items: [{ - html: '

          '+_('propertysets_desc')+'

          ' - ,id: 'modx-property-set-msg' - ,xtype: 'modx-description' - },{ - layout: 'column' - ,border: false - ,cls: 'main-wrapper' - ,items: [{ - columnWidth: .3 - ,cls: 'left-col' - ,border: false - ,layout: 'anchor' - ,items: [{ - xtype: 'modx-tree-property-sets' - ,preventRender: true - ,anchor: '100%' +MODx.panel.PropertySet = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-property-sets', + cls: 'container', + items: [{ + html: _('propertysets'), + xtype: 'modx-header' + }, MODx.getPageStructure([{ + title: _('propertysets'), + layout: 'form', + id: 'modx-property-set-form', + border: true, + items: [{ + html: `

          ${_('propertysets_desc')}

          `, + id: 'modx-property-set-msg', + xtype: 'modx-description' + }, { + layout: 'column', + border: false, + cls: 'main-wrapper', + items: [{ + columnWidth: 0.3, + cls: 'left-col', + border: false, + layout: 'anchor', + items: [{ + xtype: 'modx-tree-property-sets', + preventRender: true, + anchor: '100%' }] - },{ - columnWidth: .7 - ,layout: 'form' - ,border: false - ,autoHeight: true - ,id: 'right-column' - ,items: [] + }, { + columnWidth: 0.7, + layout: 'form', + border: false, + autoHeight: true, + id: 'right-column', + items: [] }] }] }])] }); - MODx.panel.PropertySet.superclass.constructor.call(this,config); + MODx.panel.PropertySet.superclass.constructor.call(this, config); /* load after b/c of safari/ie focus bug */ (function() { Ext.getCmp('right-column').add({ - xtype: 'modx-grid-property-set-properties' - ,id: 'modx-grid-element-properties' + xtype: 'modx-grid-property-set-properties', + id: 'modx-grid-element-properties' }); }).defer(50, this); }; -Ext.extend(MODx.panel.PropertySet,MODx.FormPanel); -Ext.reg('modx-panel-property-sets',MODx.panel.PropertySet); +Ext.extend(MODx.panel.PropertySet, MODx.FormPanel); +Ext.reg('modx-panel-property-sets', MODx.panel.PropertySet); /** * @class MODx.grid.PropertySetProperties @@ -65,43 +64,47 @@ Ext.reg('modx-panel-property-sets',MODx.panel.PropertySet); * @param {Object} config An object of config properties * @xtype modx-grid-property-set-properties */ -MODx.grid.PropertySetProperties = function(config) { - config = config || {}; - Ext.applyIf(config,{ - autoHeight: true - ,lockProperties: false - ,tbar: [{ - xtype: 'modx-combo-property-set' - ,id: 'modx-combo-property-set' - ,baseParams: { +MODx.grid.PropertySetProperties = function(config = {}) { + Ext.applyIf(config, { + autoHeight: true, + lockProperties: false, + tbar: [{ + xtype: 'modx-combo-property-set', + id: 'modx-combo-property-set', + baseParams: { action: 'Element/PropertySet/GetList' - } - ,listeners: { - 'select': {fn:function(cb) { Ext.getCmp('modx-grid-element-properties').changePropertySet(cb); },scope:this} - } - ,value: '' - },{ - text: _('property_create') - ,handler: function(btn,e) { - if (Ext.getCmp('modx-combo-property-set').value != '') { - Ext.getCmp('modx-grid-element-properties').create(btn,e); + }, + listeners: { + select: { + fn: function(cb) { + Ext.getCmp('modx-grid-element-properties').changePropertySet(cb); + }, + scope: this + } + }, + value: '' + }, { + text: _('property_create'), + handler: function(btn, e) { + if (Ext.getCmp('modx-combo-property-set').value !== '') { + Ext.getCmp('modx-grid-element-properties').create(btn, e); } else { MODx.msg.alert('', _('propertyset_err_ns')); } - } - ,scope: this - },'->',{ - text: _('propertyset_save') - ,cls: 'primary-button' - ,handler: function() { Ext.getCmp('modx-grid-element-properties').save(); } - ,scope: this + }, + scope: this + }, '->', { + text: _('propertyset_save'), + cls: 'primary-button', + handler: function() { Ext.getCmp('modx-grid-element-properties').save(); }, + scope: this }] }); Ext.getCmp('right-column').disable(); - MODx.grid.PropertySetProperties.superclass.constructor.call(this,config); + MODx.grid.PropertySetProperties.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.PropertySetProperties,MODx.grid.ElementProperties); -Ext.reg('modx-grid-property-set-properties',MODx.grid.PropertySetProperties); +Ext.extend(MODx.grid.PropertySetProperties, MODx.grid.ElementProperties); +Ext.reg('modx-grid-property-set-properties', MODx.grid.PropertySetProperties); /** * @class MODx.tree.PropertySets @@ -109,199 +112,231 @@ Ext.reg('modx-grid-property-set-properties',MODx.grid.PropertySetProperties); * @param {Object} config An object of config properties * @xtype modx-tree-property-sets */ -MODx.tree.PropertySets = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('propertysets') - ,url: MODx.config.connector_url - ,action: 'Element/PropertySet/GetNodes' - ,rootIconCls: 'icon-sitemap' - ,root_name: _('propertysets') - ,rootVisible: false - ,enableDD: false - ,tbar: ['->', { - text: _('propertyset_new') - ,cls: 'primary-button' - ,handler: this.createSet - ,scope: this - }] - ,useDefaultToolbar: true +MODx.tree.PropertySets = function(config = {}) { + Ext.applyIf(config, { + title: _('propertysets'), + url: MODx.config.connector_url, + action: 'Element/PropertySet/GetNodes', + rootIconCls: 'icon-sitemap', + root_name: _('propertysets'), + rootVisible: false, + enableDD: false, + tbar: ['->', { + text: _('propertyset_new'), + cls: 'primary-button', + handler: this.createSet, + scope: this + }], + useDefaultToolbar: true }); - MODx.tree.PropertySets.superclass.constructor.call(this,config); - this.on('click',this.loadGrid,this); + MODx.tree.PropertySets.superclass.constructor.call(this, config); + this.on('click', this.loadGrid, this); }; -Ext.extend(MODx.tree.PropertySets,MODx.tree.Tree,{ - loadGrid: function(n,e) { - Ext.getCmp('right-column').enable(); - var ar = n.id.split('_'); - if (ar[0] == 'ps') { - MODx.Ajax.request({ - url: MODx.config.connector_url - ,params: { - action: 'Element/PropertySet/GetProperties' - ,id: ar[1] +Ext.extend(MODx.tree.PropertySets, MODx.tree.Tree, { + loadGrid: function(node, e) { + const + [recordType, setId, elId, elType] = node.id.split('_'), + propsGrid = Ext.getCmp('modx-grid-element-properties'), + propSetCombo = Ext.getCmp('modx-combo-property-set'), + setGridData = (response, setId, elId = null, elType = null) => { + const + data = response.object, + store = propsGrid.getStore() + ; + propsGrid.defaultProperties = data; + if (elId && elType) { + propsGrid.config.elementId = elId; + propsGrid.config.elementType = elType; + } else { + delete propsGrid.config.elementId; + delete propsGrid.config.elementType; } - ,listeners: { - 'success': {fn:function(r) { - var d = r.object; - var g = Ext.getCmp('modx-grid-element-properties'); - var s = g.getStore(); - g.defaultProperties = d; - delete g.config.elementId; - delete g.config.elementType; - s.removeAll(); - s.loadData(d); + store.removeAll(); + store.loadData(data); + propSetCombo.setValue(setId); + } + ; + Ext.getCmp('right-column').enable(); - Ext.getCmp('modx-combo-property-set').setValue(ar[1]); - },scope:this} + if (recordType === 'ps') { + MODx.Ajax.request({ + url: MODx.config.connector_url, + params: { + action: 'Element/PropertySet/GetProperties', + id: setId + }, + listeners: { + success: { + fn: response => { + setGridData(response, setId); + } + } } }); - } else if (ar[0] == 'el' && ar[2] && ar[3]) { + } else if (recordType === 'el' && elId && elType) { MODx.Ajax.request({ - url: MODx.config.connector_url - ,params: { - action: 'Element/PropertySet/GetProperties' - ,id: ar[1] - ,element: ar[2] - ,element_class: ar[3] - } - ,listeners: { - 'success': {fn:function(r) { - var d = r.object; - var g = Ext.getCmp('modx-grid-element-properties'); - var s = g.getStore(); - g.defaultProperties = d; - g.config.elementId = ar[2]; - g.config.elementType = ar[3]; - s.removeAll(); - s.loadData(d); - - Ext.getCmp('modx-combo-property-set').setValue(ar[1]); - },scope:this} + url: MODx.config.connector_url, + params: { + action: 'Element/PropertySet/GetProperties', + id: setId, + element: elId, + element_class: elType + }, + listeners: { + success: { + fn: response => { + setGridData(response, setId, elId, elType); + } + } } }); } - } + }, - ,createSet: function(btn,e) { + createSet: function(btn, e) { if (!this.winCreateSet) { this.winCreateSet = MODx.load({ - xtype: 'modx-window-property-set-create' - ,listeners: { - 'success':{fn:function() { - this.refresh(); - Ext.getCmp('modx-combo-property-set').store.reload(); - },scope:this} + xtype: 'modx-window-property-set-create', + listeners: { + success: { + fn: function() { + this.refresh(); + Ext.getCmp('modx-combo-property-set').store.reload(); + }, + scope: this + } } }); } this.winCreateSet.reset(); this.winCreateSet.show(e.target); - } + }, - ,duplicateSet: function(btn,e) { - var id = this.cm.activeNode.id.split('_'); - var r = this.cm.activeNode.attributes.data; - r.id = id[1]; - r.new_name = _('duplicate_of',{name:r.name}); + duplicateSet: function(btn, e) { + const + [, setId] = this.cm.activeNode.id.split('_'), + record = this.cm.activeNode.attributes.data + ; + record.id = setId; + record.new_name = _('duplicate_of', { name: record.name }); if (!this.winDupeSet) { this.winDupeSet = MODx.load({ - xtype: 'modx-window-property-set-duplicate' - ,record: r - ,listeners: { - 'success':{fn:function() { - this.refresh(); - Ext.getCmp('modx-combo-property-set').store.reload(); - },scope:this} + xtype: 'modx-window-property-set-duplicate', + record: record, + listeners: { + success: { + fn: function() { + this.refresh(); + Ext.getCmp('modx-combo-property-set').store.reload(); + }, + scope: this + } } }); } - this.winDupeSet.setValues(r); + this.winDupeSet.setValues(record); this.winDupeSet.show(e.target); - } + }, - ,updateSet: function(btn,e) { - var id = this.cm.activeNode.id.split('_'); - var r = this.cm.activeNode.attributes.data; - r.id = id[1]; + updateSet: function(btn, e) { + const + [, setId] = this.cm.activeNode.id.split('_'), + record = this.cm.activeNode.attributes.data + ; + record.id = setId; if (!this.winUpdateSet) { this.winUpdateSet = MODx.load({ - xtype: 'modx-window-property-set-update' - ,record: r - ,listeners: { - 'success':{fn:function() { - this.refresh(); - Ext.getCmp('modx-combo-property-set').store.reload(); - },scope:this} + xtype: 'modx-window-property-set-update', + record: record, + listeners: { + success: { + fn: function() { + this.refresh(); + Ext.getCmp('modx-combo-property-set').store.reload(); + }, + scope: this + } } }); } - this.winUpdateSet.setValues(r); + this.winUpdateSet.setValues(record); this.winUpdateSet.show(e.target); - } + }, - ,removeSet: function(btn,e) { - var id = this.cm.activeNode.id.split('_'); - id = id[1]; + removeSet: function(btn, e) { + const [, setId] = this.cm.activeNode.id.split('_'); MODx.msg.confirm({ - text: _('propertyset_remove_confirm') - ,url: MODx.config.connector_url - ,params: { - action: 'Element/PropertySet/Remove' - ,id: id - } - ,listeners: { - 'success': {fn:function() { - this.refreshNode(this.cm.activeNode.id); - var g = Ext.getCmp('modx-grid-element-properties'); - g.getStore().removeAll(); - g.defaultProperties = []; - Ext.getCmp('modx-combo-property-set').setValue(''); - },scope:this} + text: _('propertyset_remove_confirm'), + url: MODx.config.connector_url, + params: { + action: 'Element/PropertySet/Remove', + id: setId + }, + listeners: { + success: { + fn: function() { + this.refreshNode(this.cm.activeNode.id); + const propsGrid = Ext.getCmp('modx-grid-element-properties'); + propsGrid.getStore().removeAll(); + propsGrid.defaultProperties = []; + Ext.getCmp('modx-combo-property-set').setValue(''); + }, + scope: this + } } }); - } - - ,addElement: function(btn,e) { - var id = this.cm.activeNode.id.split('_'); id = id[1]; - var t = this.cm.activeNode.text; - var r = { - propertysetName: this.cm.activeNode.text - ,propertyset: id - }; + }, + addElement: function(btn, e) { + const + [, setId] = this.cm.activeNode.id.split('_'), + record = { + propertysetName: this.cm.activeNode.text, + propertyset: setId + } + ; if (!this.winPSEA) { this.winPSEA = MODx.load({ - xtype: 'modx-window-propertyset-element-add' - ,record: r - ,listeners: { - 'success':{fn:function() { this.refreshNode(this.cm.activeNode.id,true); },scope:this} + xtype: 'modx-window-propertyset-element-add', + record: record, + listeners: { + success: { + fn: function() { + this.refreshNode(this.cm.activeNode.id, true); + }, + scope: this + } } }); } this.winPSEA.fp.getForm().reset(); - this.winPSEA.fp.getForm().setValues(r); + this.winPSEA.fp.getForm().setValues(record); this.winPSEA.show(e.target); - } + }, - ,removeElement: function(btn,e) { - var d = this.cm.activeNode.attributes; + removeElement: function(btn, e) { + const { attributes } = this.cm.activeNode; MODx.msg.confirm({ - text: _('propertyset_element_remove_confirm') - ,url: MODx.config.connector_url - ,params: { - action: 'Element/PropertySet/RemoveElement' - ,element: d.pk - ,element_class: d.element_class - ,propertyset: d.propertyset - } - ,listeners: { - 'success': {fn:function() { this.refreshNode(this.cm.activeNode.id); },scope:this} + text: _('propertyset_element_remove_confirm'), + url: MODx.config.connector_url, + params: { + action: 'Element/PropertySet/RemoveElement', + element: attributes.pk, + element_class: attributes.element_class, + propertyset: attributes.propertyset + }, + listeners: { + success: { + fn: function() { + this.refreshNode(this.cm.activeNode.id); + }, + scope: this + } } }); } }); -Ext.reg('modx-tree-property-sets',MODx.tree.PropertySets); +Ext.reg('modx-tree-property-sets', MODx.tree.PropertySets); /** * @class MODx.window.AddElementToPropertySet @@ -309,60 +344,61 @@ Ext.reg('modx-tree-property-sets',MODx.tree.PropertySets); * @param {Object} config An object of configuration properties * @xtype modx-window-propertyset-element-add */ -MODx.window.AddElementToPropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('propertyset_element_add') - ,url: MODx.config.connector_url - ,baseParams: { +MODx.window.AddElementToPropertySet = function(config = {}) { + Ext.applyIf(config, { + title: _('propertyset_element_add'), + url: MODx.config.connector_url, + baseParams: { action: 'Element/PropertySet/AddElement' - } - ,fields: [{ - xtype: 'hidden' - ,name: 'propertyset' - },{ - xtype: 'statictextfield' - ,fieldLabel: _('propertyset') - ,name: 'propertysetName' - ,anchor: '100%' - },{ - xtype: 'modx-combo-element-class' - ,fieldLabel: _('class_name') - ,name: 'element_class' - ,id: 'modx-combo-element-class' - ,anchor: '100%' - ,listeners: { - 'select': {fn:this.onClassSelect,scope:this} + }, + fields: [{ + xtype: 'hidden', + name: 'propertyset' + }, { + xtype: 'statictextfield', + fieldLabel: _('propertyset'), + name: 'propertysetName', + anchor: '100%' + }, { + xtype: 'modx-combo-element-class', + fieldLabel: _('class_name'), + name: 'element_class', + id: 'modx-combo-element-class', + anchor: '100%', + listeners: { + select: { fn: this.onClassSelect, scope: this } } - },{ - xtype: 'modx-combo-elements' - ,fieldLabel: _('element') - ,name: 'element' - ,id: 'modx-combo-elements' - ,anchor: '100%' - ,listeners: { - 'select': {fn:this.onElementSelect,scope:this} + }, { + xtype: 'modx-combo-elements', + fieldLabel: _('element'), + name: 'element', + id: 'modx-combo-elements', + anchor: '100%', + listeners: { + select: { fn: this.onElementSelect, scope: this } } }] }); - MODx.window.AddElementToPropertySet.superclass.constructor.call(this,config); + MODx.window.AddElementToPropertySet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.AddElementToPropertySet,MODx.Window,{ - onClassSelect: function(cb) { - var e = Ext.getCmp('modx-combo-elements'); - var s = e.store; - s.baseParams.element_class = cb.getValue(); - s.load(); - e.setValue(''); - } - ,onElementSelect: function(cb) { - var ec = Ext.getCmp('modx-combo-element-class'); - if (ec.getValue() === '') { - ec.setValue('MODX\\Revolution\\modSnippet'); +Ext.extend(MODx.window.AddElementToPropertySet, MODx.Window, { + onClassSelect: function(classCombo) { + const + elCombo = Ext.getCmp('modx-combo-elements'), + { store } = elCombo + ; + store.baseParams.element_class = classCombo.getValue(); + store.load(); + elCombo.setValue(''); + }, + onElementSelect: function(elCombo) { + const elType = Ext.getCmp('modx-combo-element-class'); + if (elType.getValue() === '') { + elType.setValue('MODX\\Revolution\\modSnippet'); } } }); -Ext.reg('modx-window-propertyset-element-add',MODx.window.AddElementToPropertySet); +Ext.reg('modx-window-propertyset-element-add', MODx.window.AddElementToPropertySet); /** * @class MODx.combo.ElementClass @@ -370,25 +406,24 @@ Ext.reg('modx-window-propertyset-element-add',MODx.window.AddElementToPropertySe * @param {Object} config An object of configuration properties * @xtype modx-combo-element-class */ -MODx.combo.ElementClass = function(config) { - config = config || {}; - Ext.applyIf(config,{ - name: 'element_class' - ,hiddenName: 'element_class' - ,displayField: 'name' - ,valueField: 'name' - ,fields: ['name'] - ,pageSize: 20 - ,editable: false - ,url: MODx.config.connector_url - ,baseParams: { +MODx.combo.ElementClass = function(config = {}) { + Ext.applyIf(config, { + name: 'element_class', + hiddenName: 'element_class', + displayField: 'name', + valueField: 'name', + fields: ['name'], + pageSize: 20, + editable: false, + url: MODx.config.connector_url, + baseParams: { action: 'Element/GetClasses' } }); - MODx.combo.ElementClass.superclass.constructor.call(this,config); + MODx.combo.ElementClass.superclass.constructor.call(this, config); }; -Ext.extend(MODx.combo.ElementClass,MODx.combo.ComboBox); -Ext.reg('modx-combo-element-class',MODx.combo.ElementClass); +Ext.extend(MODx.combo.ElementClass, MODx.combo.ComboBox); +Ext.reg('modx-combo-element-class', MODx.combo.ElementClass); /** * @class MODx.combo.Elements @@ -396,26 +431,25 @@ Ext.reg('modx-combo-element-class',MODx.combo.ElementClass); * @param {Object} config An object of configuration properties * @xtype modx-combo-elements */ -MODx.combo.Elements = function(config) { - config = config || {}; - Ext.applyIf(config,{ - name: 'element' - ,hiddenName: 'element' - ,displayField: 'name' - ,valueField: 'id' - ,fields: ['id','name'] - ,pageSize: 20 - ,editable: false - ,url: MODx.config.connector_url - ,baseParams: { - action: 'Element/GetListByClass' - ,element_class: 'MODX\\Revolution\\modSnippet' +MODx.combo.Elements = function(config = {}) { + Ext.applyIf(config, { + name: 'element', + hiddenName: 'element', + displayField: 'name', + valueField: 'id', + fields: ['id', 'name'], + pageSize: 20, + editable: false, + url: MODx.config.connector_url, + baseParams: { + action: 'Element/GetListByClass', + element_class: 'MODX\\Revolution\\modSnippet' } }); - MODx.combo.Elements.superclass.constructor.call(this,config); + MODx.combo.Elements.superclass.constructor.call(this, config); }; -Ext.extend(MODx.combo.Elements,MODx.combo.ComboBox); -Ext.reg('modx-combo-elements',MODx.combo.Elements); +Ext.extend(MODx.combo.Elements, MODx.combo.ComboBox); +Ext.reg('modx-combo-elements', MODx.combo.Elements); /** * @class MODx.window.CreatePropertySet @@ -423,45 +457,43 @@ Ext.reg('modx-combo-elements',MODx.combo.Elements); * @param {Object} config An object of configuration properties * @xtype modx-window-property-set-create */ -MODx.window.CreatePropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('propertyset_create') - ,url: MODx.config.connector_url - ,baseParams: { +MODx.window.CreatePropertySet = function(config = {}) { + Ext.applyIf(config, { + title: _('propertyset_create'), + url: MODx.config.connector_url, + baseParams: { action: 'Element/PropertySet/Create' - } - ,autoHeight: true - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - },{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'name' - ,anchor: '100%' - ,allowBlank: false - ,maxLength: 50 - },{ - xtype: 'modx-combo-category' - ,fieldLabel: _('category') - ,name: 'category' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,anchor: '100%' - ,grow: true - ,maxLength: 255 - }] - ,keys: [] + }, + autoHeight: true, + fields: [{ + xtype: 'hidden', + name: 'id' + }, { + xtype: 'textfield', + fieldLabel: _('name'), + name: 'name', + anchor: '100%', + allowBlank: false, + maxLength: 50 + }, { + xtype: 'modx-combo-category', + fieldLabel: _('category'), + name: 'category', + anchor: '100%' + }, { + xtype: 'textarea', + fieldLabel: _('description'), + name: 'description', + anchor: '100%', + grow: true, + maxLength: 255 + }], + keys: [] }); - MODx.window.CreatePropertySet.superclass.constructor.call(this,config); + MODx.window.CreatePropertySet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreatePropertySet,MODx.Window); -Ext.reg('modx-window-property-set-create',MODx.window.CreatePropertySet); +Ext.extend(MODx.window.CreatePropertySet, MODx.Window); +Ext.reg('modx-window-property-set-create', MODx.window.CreatePropertySet); /** * @class MODx.window.UpdatePropertySet @@ -469,19 +501,18 @@ Ext.reg('modx-window-property-set-create',MODx.window.CreatePropertySet); * @param {Object} config An object of configuration properties * @xtype modx-window-property-set-update */ -MODx.window.UpdatePropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('propertyset_update') - ,baseParams: { +MODx.window.UpdatePropertySet = function(config = {}) { + Ext.applyIf(config, { + title: _('propertyset_update'), + baseParams: { action: 'Element/PropertySet/Update' - } - ,autoHeight: true + }, + autoHeight: true }); - MODx.window.UpdatePropertySet.superclass.constructor.call(this,config); + MODx.window.UpdatePropertySet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.UpdatePropertySet,MODx.window.CreatePropertySet); -Ext.reg('modx-window-property-set-update',MODx.window.UpdatePropertySet); +Ext.extend(MODx.window.UpdatePropertySet, MODx.window.CreatePropertySet); +Ext.reg('modx-window-property-set-update', MODx.window.UpdatePropertySet); /** * @class MODx.window.DuplicatePropertySet @@ -489,36 +520,35 @@ Ext.reg('modx-window-property-set-update',MODx.window.UpdatePropertySet); * @param {Object} config An object of configuration properties * @xtype modx-window-property-set-duplicate */ -MODx.window.DuplicatePropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('propertyset_duplicate') - ,url: MODx.config.connector_url - ,baseParams: { +MODx.window.DuplicatePropertySet = function(config = {}) { + Ext.applyIf(config, { + title: _('propertyset_duplicate'), + url: MODx.config.connector_url, + baseParams: { action: 'Element/PropertySet/Duplicate' - } - ,autoHeight: true - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-dpropset-id' - },{ - xtype: 'textfield' - ,fieldLabel: _('new_name') - ,name: 'name' - ,anchor: '100%' - ,value: _('duplicate_of',{name:config.record.name}) - ,maxLength: 50 - },{ - xtype: 'xcheckbox' - ,boxLabel: _('propertyset_duplicate_copyels') - ,hideLabel: true - ,name: 'copyels' - ,id: 'modx-dpropset-copyels' - ,checked: true + }, + autoHeight: true, + fields: [{ + xtype: 'hidden', + name: 'id', + id: 'modx-dpropset-id' + }, { + xtype: 'textfield', + fieldLabel: _('new_name'), + name: 'name', + anchor: '100%', + value: _('duplicate_of', { name: config.record.name }), + maxLength: 50 + }, { + xtype: 'xcheckbox', + boxLabel: _('propertyset_duplicate_copyels'), + hideLabel: true, + name: 'copyels', + id: 'modx-dpropset-copyels', + checked: true }] }); - MODx.window.DuplicatePropertySet.superclass.constructor.call(this,config); + MODx.window.DuplicatePropertySet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.DuplicatePropertySet,MODx.Window); -Ext.reg('modx-window-property-set-duplicate',MODx.window.DuplicatePropertySet); +Ext.extend(MODx.window.DuplicatePropertySet, MODx.Window); +Ext.reg('modx-window-property-set-duplicate', MODx.window.DuplicatePropertySet); From e9a8bdb668696f767a7149e410b38da905b54008 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 25 Nov 2024 14:47:00 -0500 Subject: [PATCH 34/39] Update modx.grid.element.properties.js Formatting, code style & optimization changes only --- .../element/modx.grid.element.properties.js | 1853 ++++++++--------- 1 file changed, 897 insertions(+), 956 deletions(-) diff --git a/manager/assets/modext/widgets/element/modx.grid.element.properties.js b/manager/assets/modext/widgets/element/modx.grid.element.properties.js index 92e04f45f6..3064864c3b 100644 --- a/manager/assets/modext/widgets/element/modx.grid.element.properties.js +++ b/manager/assets/modext/widgets/element/modx.grid.element.properties.js @@ -1,238 +1,263 @@ -MODx.panel.ElementProperties = function(config) { - config = config || {}; - Ext.applyIf(config,{ - id: 'modx-panel-element-properties' - ,title: _('properties') - ,header: false - ,defaults: { collapsible: false ,autoHeight: true ,border: false } - ,layout: 'form' - ,items: [{ - html: '

          '+_('element_properties_desc')+'

          ' - ,itemId: 'desc-properties' - ,xtype: 'modx-description' - },{ - xtype: 'modx-grid-element-properties' - ,cls:'main-wrapper' - ,id: 'modx-grid-element-properties' - ,itemId: 'grid-properties' - ,autoHeight: true - ,border: true - ,panel: config.elementPanel - ,elementId: config.elementId - ,elementType: config.elementType - },{ - layout: 'form' - ,labelAlign: 'top' - ,border: false - ,cls: 'main-wrapper' - ,items: [{ - xtype: 'xcheckbox' - ,boxLabel: _('property_preprocess') - ,description: MODx.expandHelp ? '' : _('property_preprocess_msg') - ,name: 'property_preprocess' - ,id: 'modx-element-property-preprocess' - ,inputValue: true - ,hideLabel: true - ,checked: config.record.property_preprocess || 0 - ,listeners: { - 'check':{fn:function() {Ext.getCmp(this.config.elementPanel).markDirty();},scope:this} +MODx.panel.ElementProperties = function(config = {}) { + Ext.applyIf(config, { + id: 'modx-panel-element-properties', + title: _('properties'), + header: false, + defaults: { + collapsible: false, + autoHeight: true, + border: false + }, + layout: 'form', + items: [{ + html: `

          ${_('element_properties_desc')}

          `, + itemId: 'desc-properties', + xtype: 'modx-description' + }, { + xtype: 'modx-grid-element-properties', + cls: 'main-wrapper', + id: 'modx-grid-element-properties', + itemId: 'grid-properties', + autoHeight: true, + border: true, + panel: config.elementPanel, + elementId: config.elementId, + elementType: config.elementType + }, { + layout: 'form', + labelAlign: 'top', + border: false, + cls: 'main-wrapper', + items: [{ + xtype: 'xcheckbox', + boxLabel: _('property_preprocess'), + description: MODx.expandHelp ? '' : _('property_preprocess_msg'), + name: 'property_preprocess', + inputValue: true, + hideLabel: true, + checked: config.record.property_preprocess || 0, + listeners: { + check: { + fn: function() { + Ext.getCmp(this.config.elementPanel).markDirty(); + }, + scope: this + } } - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-element-property-preprocess' - ,html: _('property_preprocess_msg') - ,cls: 'desc-under' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('property_preprocess_msg'), + cls: 'desc-under' }] }] }); - MODx.panel.ElementProperties.superclass.constructor.call(this,config); + MODx.panel.ElementProperties.superclass.constructor.call(this, config); }; -Ext.extend(MODx.panel.ElementProperties,MODx.Panel); -Ext.reg('modx-panel-element-properties',MODx.panel.ElementProperties); - +Ext.extend(MODx.panel.ElementProperties, MODx.Panel); +Ext.reg('modx-panel-element-properties', MODx.panel.ElementProperties); -MODx.grid.ElementProperties = function(config) { - config = config || {}; +MODx.grid.ElementProperties = function(config = {}) { this.exp = new Ext.grid.RowExpander({ - tpl : new Ext.Template( + tpl: new Ext.Template( '

          {desc_trans}

          ' ) }); - Ext.applyIf(config,{ - title: _('properties') - ,id: 'modx-grid-element-properties' - ,maxHeight: 300 - ,fields: ['name','desc','xtype','options','value','lexicon','overridden','desc_trans','area','area_trans'] - ,autoExpandColumn: 'value' - ,sortBy: 'name' - ,anchor: '100%' - ,sm: new Ext.grid.RowSelectionModel({singleSelect:false}) - ,loadMask: true - ,lockProperties: true - ,plugins: [this.exp] - ,grouping: true - ,groupBy: 'area_trans' - ,singleText: _('property') - ,pluralText: _('properties') - ,columns: [this.exp,{ - header: _('name') - ,dataIndex: 'name' - ,width: 200 - ,sortable: true - ,renderer: this._renderName - },{ - header: _('type') - ,dataIndex: 'xtype' - ,width: 100 - ,renderer: this._renderType - ,sortable: true - },{ - header: _('value') - ,dataIndex: 'value' - ,id: 'value' - ,width: 250 - ,renderer: this.renderDynField.createDelegate(this,[this],true) - ,sortable: true - },{ - header: _('area') - ,dataIndex: 'area_trans' - ,id: 'area' - ,width: 150 - ,sortable: true - ,hidden: true - }] - ,tbar: [{ - text: _('property_create') - ,id: 'modx-btn-property-create' - ,handler: this.create - ,scope: this - ,disabled: true - },{ - text: _('properties_default_locked') - ,id: 'modx-btn-propset-lock' - ,handler: this.togglePropertiesLock - ,enableToggle: true - ,pressed: true - ,disabled: MODx.perm.unlock_element_properties ? false : true - ,scope: this - },'->',{ - xtype: 'modx-combo-property-set' - ,id: 'modx-combo-property-set' - ,baseParams: { - action: 'Element/PropertySet/GetList' - ,showAssociated: true - ,elementId: config.elementId - ,elementType: config.elementType - } - ,value: 0 - ,listeners: { - 'select': {fn:this.changePropertySet,scope:this} + Ext.applyIf(config, { + title: _('properties'), + id: 'modx-grid-element-properties', + maxHeight: 300, + fields: [ + 'name', + 'desc', + 'xtype', + 'options', + 'value', + 'lexicon', + 'overridden', + 'desc_trans', + 'area', + 'area_trans' + ], + autoExpandColumn: 'value', + sortBy: 'name', + anchor: '100%', + sm: new Ext.grid.RowSelectionModel({ + singleSelect: false + }), + loadMask: true, + lockProperties: true, + plugins: [this.exp], + grouping: true, + groupBy: 'area_trans', + singleText: _('property'), + pluralText: _('properties'), + columns: [this.exp, { + header: _('name'), + dataIndex: 'name', + width: 200, + sortable: true, + renderer: this._renderName + }, { + header: _('type'), + dataIndex: 'xtype', + width: 100, + renderer: this._renderType, + sortable: true + }, { + header: _('value'), + dataIndex: 'value', + id: 'value', + width: 250, + renderer: this.renderDynField.createDelegate(this, [this], true), + sortable: true + }, { + header: _('area'), + dataIndex: 'area_trans', + id: 'area', + width: 150, + sortable: true, + hidden: true + }], + tbar: [{ + text: _('property_create'), + id: 'modx-btn-property-create', + handler: this.create, + scope: this, + disabled: true + }, { + text: _('properties_default_locked'), + id: 'modx-btn-propset-lock', + handler: this.togglePropertiesLock, + enableToggle: true, + pressed: true, + disabled: !MODx.perm.unlock_element_properties, + scope: this + }, '->', { + xtype: 'modx-combo-property-set', + id: 'modx-combo-property-set', + baseParams: { + action: 'Element/PropertySet/GetList', + showAssociated: true, + elementId: config.elementId, + elementType: config.elementType + }, + value: 0, + listeners: { + select: { + fn: this.changePropertySet, + scope: this + } } - },{ - text: _('propertyset_add') - ,handler: this.addPropertySet - ,scope: this - },{ - text: _('propertyset_save') - ,cls: 'primary-button' - ,handler: this.save - ,scope: this - ,hidden: MODx.request.id ? false : true - }] - ,bbar: [{ - text: _('property_revert_all') - ,id: 'modx-btn-property-revert-all' - ,handler: this.revertAll - ,scope:this - ,disabled: true - },{ - text: _('import') - ,handler: this.importProperties - ,scope: this - },{ - text: _('export') - ,handler: this.exportProperties - ,scope: this - }] - ,collapseFirst: false - ,tools: [{ - id: 'plus' - ,qtip: _('expand_all') - ,handler: this.expandAll - ,scope: this - },{ - id: 'minus' - ,hidden: true - ,qtip: _('collapse_all') - ,handler: this.collapseAll - ,scope: this + }, { + text: _('propertyset_add'), + handler: this.addPropertySet, + scope: this + }, { + text: _('propertyset_save'), + cls: 'primary-button', + handler: this.save, + scope: this, + hidden: !MODx.request.id + }], + bbar: [{ + text: _('property_revert_all'), + id: 'modx-btn-property-revert-all', + handler: this.revertAll, + scope: this, + disabled: true + }, { + text: _('import'), + handler: this.importProperties, + scope: this + }, { + text: _('export'), + handler: this.exportProperties, + scope: this + }], + collapseFirst: false, + tools: [{ + id: 'plus', + qtip: _('expand_all'), + handler: this.expandAll, + scope: this + }, { + id: 'minus', + hidden: true, + qtip: _('collapse_all'), + handler: this.collapseAll, + scope: this }] }); - MODx.grid.ElementProperties.superclass.constructor.call(this,config); + MODx.grid.ElementProperties.superclass.constructor.call(this, config); this.on('afteredit', this.propertyChanged, this); this.on('afterRemoveRow', this.propertyChanged, this); - this.on('render',function() { + this.on('render', function() { this.mask = new Ext.LoadMask(this.getEl()); - },this); + }, this); if (this.config.lockProperties) { - this.on('render',function() { + this.on('render', function() { this.lockMask = MODx.load({ - xtype: 'modx-lockmask' - ,el: this.getGridEl() - ,msg: _('properties_default_locked') + xtype: 'modx-lockmask', + el: this.getGridEl(), + msg: _('properties_default_locked') }); this.lockMask.toggle(); - },this); + }, this); } }; -Ext.extend(MODx.grid.ElementProperties,MODx.grid.LocalProperty,{ - defaultProperties: [] +Ext.extend(MODx.grid.ElementProperties, MODx.grid.LocalProperty, { + defaultProperties: [], - ,onDirty: function() { + onDirty: function() { if (this.config.panel) { Ext.getCmp(this.config.panel).fireEvent('fieldChange'); } - } + }, - ,_renderType: function(v,md,rec,ri) { - switch (v) { - case 'combo-boolean': return _('yesno'); break; - case 'datefield': return _('date'); break; - case 'numberfield': return _('integer'); break; - case 'file': return _('file'); break; - case 'color': return _('color'); break; + _renderType: function(value, metaData, record, rowIndex) { + switch (value) { + case 'combo-boolean': return _('yesno'); + case 'datefield': return _('date'); + case 'numberfield': return _('integer'); + case 'file': return _('file'); + case 'color': return _('color'); + // no default } - return _(v); - } - ,_renderName: function(v,md,rec,ri) { - switch (rec.data.overridden) { + return _(value); + }, + + _renderName: function(value, metaData, record, rowIndex) { + switch (record.data.overridden) { case 1: - return ''+v+''; break; + return `${value}`; case 2: - return ''+v+''; + return `${value}`; default: - return ''+v+''; + return `${value}`; } - } + }, - ,save: function() { - var d = this.encode(); - var cb = Ext.getCmp('modx-combo-property-set'); - if (!cb) { + save: function() { + const + data = this.encode(), + propSetCombo = Ext.getCmp('modx-combo-property-set') + ; + if (!propSetCombo) { this.getStore().commitChanges(); this.onDirty(); return true; } - var p = { - action: 'Element/PropertySet/UpdateFromElement' - ,id: cb.getValue() - ,data: d + const params = { + action: 'Element/PropertySet/UpdateFromElement', + id: propSetCombo.getValue(), + data: data }; if (this.config.elementId) { - Ext.apply(p,{ - elementId: this.config.elementId - ,elementType: this.config.elementType + Ext.apply(params, { + elementId: this.config.elementId, + elementType: this.config.elementType }); } try { @@ -240,361 +265,419 @@ Ext.extend(MODx.grid.ElementProperties,MODx.grid.LocalProperty,{ this.mask = new Ext.LoadMask(this.getEl()); } if (this.mask) { this.mask.show(); } - } catch (e) { } + // eslint-disable-next-line no-empty + } catch (e) {} MODx.Ajax.request({ - url: MODx.config.connector_url - ,params: p - ,listeners: { - 'success': {fn:function(r) { - this.getStore().commitChanges(); - this.changePropertySet(cb); - this.onDirty(); - if (this.mask) { this.mask.hide(); } - MODx.msg.status({ - title: _('success') - ,message: _('save_successful') - ,dontHide: r.message != '' ? true : false - }); - },scope:this} + url: MODx.config.connector_url, + params: params, + listeners: { + success: { + fn: function(response) { + this.getStore().commitChanges(); + this.changePropertySet(propSetCombo); + this.onDirty(); + if (this.mask) { this.mask.hide(); } + MODx.msg.status({ + title: _('success'), + message: _('save_successful'), + dontHide: !Ext.isEmpty(response.message) + }); + }, + scope: this + } } }); - } + }, - ,addPropertySet: function(btn,e) { - this.loadWindow(btn,e,{ - xtype: 'modx-window-element-property-set-add' - ,record: { - elementId: this.config.elementId != 0 ? this.config.elementId : '' - ,elementType: this.config.elementType - } - ,listeners: { - 'success': {fn:function(o) { - var cb = Ext.getCmp('modx-combo-property-set'); - cb.store.reload({ - callback: function() { - cb.setValue(o.a.result.object.id); - this.changePropertySet(cb); - } - ,scope: this - }); - this.onDirty(); - },scope:this} + addPropertySet: function(btn, e) { + this.loadWindow(btn, e, { + xtype: 'modx-window-element-property-set-add', + record: { + elementId: this.config.elementId !== 0 ? this.config.elementId : '', + elementType: this.config.elementType + }, + listeners: { + success: { + fn: function(response) { + const propSetCombo = Ext.getCmp('modx-combo-property-set'); + propSetCombo.store.reload({ + callback: function() { + propSetCombo.setValue(response.a.result.object.id); + this.changePropertySet(propSetCombo); + }, + scope: this + }); + this.onDirty(); + }, + scope: this + } } }); - } + }, - ,togglePropertiesLock: function() { - var ps = Ext.getCmp('modx-combo-property-set').getValue(); - if (ps == 0 || ps == _('default')) { - Ext.getCmp('modx-btn-propset-lock').setText(this.lockMask.locked ? _('properties_default_unlocked') : _('properties_default_locked')); + togglePropertiesLock: function() { + const propSetId = Ext.getCmp('modx-combo-property-set').getValue(); + if (propSetId === 0 || propSetId === _('default')) { + Ext.getCmp('modx-btn-propset-lock').setText(this.lockMask.locked + ? _('properties_default_unlocked') + : _('properties_default_locked')) + ; this.lockMask.toggle(); this.toggleButtons(this.lockMask.locked); } - } + }, - ,toggleButtons: function(v) { - var btn = Ext.getCmp('modx-btn-property-create'); + toggleButtons: function(value) { + const btn = Ext.getCmp('modx-btn-property-create'); if (btn) { - Ext.getCmp('modx-btn-property-create').setDisabled(v); - Ext.getCmp('modx-btn-property-revert-all').setDisabled(v); + Ext.getCmp('modx-btn-property-create').setDisabled(value); + Ext.getCmp('modx-btn-property-revert-all').setDisabled(value); } - } + }, - ,changePropertySet: function(cb) { - var ps = cb.getValue(); - var lockbtn = Ext.getCmp('modx-btn-propset-lock'); - if (ps == 0 || ps == _('default')) { + changePropertySet: function(propSetCombo) { + const + propSetId = propSetCombo.getValue(), + lockbtn = Ext.getCmp('modx-btn-propset-lock') + ; + if (propSetId === 0 || propSetId === _('default')) { if (MODx.perm.unlock_element_properties) { - if (lockbtn) { lockbtn.setDisabled(false); } + if (lockbtn) { + lockbtn.setDisabled(false); + } } if (this.lockMask && this.lockMask.locked) { this.lockMask.show(); this.toggleButtons(true); } } else { - if (lockbtn) { lockbtn.setDisabled(true); } - if (this.lockMask) this.lockMask.hide(); + if (lockbtn) { + lockbtn.setDisabled(true); + } + if (this.lockMask) { + this.lockMask.hide(); + } this.toggleButtons(false); } MODx.Ajax.request({ - url: MODx.config.connector_url - ,params: { - action: 'Element/PropertySet/Get' - ,id: ps - ,elementId: this.config.elementId - ,elementType: this.config.elementType - } - ,listeners: { - 'success': {fn:function(r) { - var s = this.getStore(); - var data = Ext.decode(r.object.data); - s.removeAll(); - s.loadData(data); - },scope:this} + url: MODx.config.connector_url, + params: { + action: 'Element/PropertySet/Get', + id: propSetId, + elementId: this.config.elementId, + elementType: this.config.elementType + }, + listeners: { + success: { + fn: function(response) { + const + store = this.getStore(), + data = Ext.decode(response.object.data) + ; + store.removeAll(); + store.loadData(data); + }, + scope: this + } } }); - } + }, - ,create: function(btn,e) { - this.loadWindow(btn,e,{ - xtype: 'modx-window-element-property-create' - ,blankValues: true - ,listeners: { - 'success': {fn:function(r) { - - var rec = new this.propRecord({ - name: r.name - ,desc: r.desc - ,desc_trans: r.desc - ,xtype: r.xtype - ,options: r.options - ,value: r.value - ,lexicon: r.lexicon - ,overridden: this.isDefaultPropSet() ? 0 : 2 - ,area: r.area - ,area_trans: r.area - }); - this.getStore().add(rec); - this.propertyChanged(); - this.onDirty(); - },scope:this} + create: function(btn, e) { + this.loadWindow(btn, e, { + xtype: 'modx-window-element-property-create', + blankValues: true, + listeners: { + success: { + fn: function(response) { + const record = new this.propRecord({ + name: response.name, + desc: response.desc, + desc_trans: response.desc, + xtype: response.xtype, + options: response.options, + value: response.value, + lexicon: response.lexicon, + overridden: this.isDefaultPropSet() ? 0 : 2, + area: response.area, + area_trans: response.area + }); + this.getStore().add(record); + this.propertyChanged(); + this.onDirty(); + }, + scope: this + } } }); - } + }, - ,update: function(btn,e) { - this.loadWindow(btn,e,{ - xtype: 'modx-window-element-property-update' - ,record: this.menu.record - ,listeners: { - 'success': {fn:function(r) { - var def = this.isDefaultPropSet(); - var s = this.getStore(); - var rec = s.getAt(this.menu.recordIndex); - rec.set('name',r.name); - rec.set('desc',r.desc); - rec.set('desc_trans', r.desc); - rec.set('xtype',r.xtype); - rec.set('options',r.options); - rec.set('value',r.value); - rec.set('lexicon',r.lexicon); - rec.set('overridden',r.overridden == 2 ? 2 : (!def ? 1 : 0)); - rec.set('area',r.area); - rec.set('area_trans',r.area); - this.getView().refresh(); - this.onDirty(); - },scope:this} + update: function(btn, e) { + this.loadWindow(btn, e, { + xtype: 'modx-window-element-property-update', + record: this.menu.record, + listeners: { + success: { + fn: function(response) { + const + isDefaultSet = this.isDefaultPropSet(), + store = this.getStore(), + record = store.getAt(this.menu.recordIndex) + ; + record.set('name', response.name); + record.set('desc', response.desc); + record.set('desc_trans', response.desc); + record.set('xtype', response.xtype); + record.set('options', response.options); + record.set('value', response.value); + record.set('lexicon', response.lexicon); + // eslint-disable-next-line no-nested-ternary + record.set('overridden', response.overridden === 2 ? 2 : (!isDefaultSet ? 1 : 0)); + record.set('area', response.area); + record.set('area_trans', response.area); + this.getView().refresh(); + this.onDirty(); + }, + scope: this + } } }); - } + }, - ,revert: function(btn,e) { - Ext.Msg.confirm(_('warning'),_('property_revert_confirm'),function(e) { - if (e == 'yes') { - var ri = this.menu.recordIndex; - var d = this.defaultProperties[ri]; - if (d) { - var rec = this.getStore().getAt(ri); - rec.set('name',d[0]); - rec.set('desc',d[1]); - rec.set('desc_trans',d[1]); - rec.set('xtype',d[2]); - rec.set('options',d[3]); - rec.set('value',d[4]); - rec.set('overridden',0); - rec.set('area',d[5]); - rec.set('area_trans',d[5]); - rec.commit(); + revert: function(btn, e) { + Ext.Msg.confirm(_('warning'), _('property_revert_confirm'), function(e) { + if (e === 'yes') { + const + { recordIndex } = this.menu, + propData = this.defaultProperties[recordIndex] + ; + if (propData) { + const record = this.getStore().getAt(recordIndex); + record.set('name', propData[0]); + record.set('desc', propData[1]); + record.set('desc_trans', propData[1]); + record.set('xtype', propData[2]); + record.set('options', propData[3]); + record.set('value', propData[4]); + record.set('overridden', 0); + record.set('area', propData[5]); + record.set('area_trans', propData[5]); + record.commit(); } } - },this); - } + }, this); + }, - ,revertAll: function(btn,e) { - Ext.Msg.confirm(_('warning'),_('property_revert_all_confirm'),function(e) { - if (e == 'yes') { + revertAll: function(btn, e) { + Ext.Msg.confirm(_('warning'), _('property_revert_all_confirm'), function(e) { + if (e === 'yes') { this.getStore().loadData(this.defaultProperties); } - },this); - } + }, this); + }, - ,removeMultiple: function(btn,e) { - var rows = this.getSelectionModel().getSelections(); - var rids = []; - for (var i=0;i in values, desc */ - for (var i in data) { - if (data[i][4]) { data[i][4] = data[i][4].replace(/>/g,'>').replace(/</g,'<'); } - if (data[i][5]) { data[i][5] = data[i][5].replace(/>/g,'>').replace(/</g,'<'); } - if (data[i][1]) { data[i][1] = data[i][1].replace(/>/g,'>').replace(/</g,'<'); } - } - s.loadData(data); - /* mark fields dirty */ - var recs = s.getRange(0,s.getTotalCount()); - for (var i=0;i { + [4, 5, 1].forEach(index => { + if (record[index]) { + record[index] = record[index].replace(/>/g, '>').replace(/</g, '<'); + } + }); + }); + store.loadData(data); + const newRecords = store.getRange(0, store.getTotalCount()); + newRecords.forEach(record => record.markDirty()); + this.getView().refresh(); + }, + scope: this + } } }); - } - - ,_showMenu: function(g,ri,e) { - var sm = this.getSelectionModel(); - if (sm.getSelections().length > 1) { - e.stopEvent(); - e.preventDefault(); - this.menu.removeAll(); - this.addContextMenuItem([{ - text: _('properties_remove') - ,handler: this.removeMultiple - ,scope: this - }]); - this.menu.show(e.target); - } else { - MODx.grid.ElementProperties.superclass._showMenu.call(this,g,ri,e); - } - } + }, - ,isDefaultPropSet: function() { - var ps = Ext.getCmp('modx-combo-property-set').getValue(); - return (ps == 0 || ps == _('default')); - } - - ,getMenu: function() { - var def = this.isDefaultPropSet(); - - var r = this.menu.record; - var m = []; - m.push({ - text: _('property_update') - ,scope: this - ,handler: this.update - }); + isDefaultPropSet: function() { + const propSetId = Ext.getCmp('modx-combo-property-set').getValue(); + return (propSetId === 0 || propSetId === _('default')); + }, - if (r.overridden) { - m.push({ - text: _('property_revert') - ,scope: this - ,handler: this.revert + getMenu: function() { + const + isDefaultSet = this.isDefaultPropSet(), + model = this.getSelectionModel(), + record = model.getSelected(), + propIsCustom = record.data.overridden === 2, + propIsOverriden = record.data.overridden === 1, + propUnchanged = [0, false].includes(record.data.overridden), + menu = [] + ; + if (model.getCount() > 1) { + menu.push({ + text: _('properties_remove'), + handler: this.removeMultiple, + scope: this }); - } - if ((r.overridden == 2 && !def) || (r.overridden != 1 && def) || (!r.overridden && !def)) { - m.push({ - text: _('property_remove') - ,scope: this - ,handler: this.remove.createDelegate(this,[{ - title: _('warning') - ,text: _('property_remove_confirm') - }]) + } else { + menu.push({ + text: _('property_update'), + scope: this, + handler: this.update }); + if (propIsOverriden) { + menu.push({ + text: _('property_revert'), + scope: this, + handler: this.revert + }); + } + if ( + (!isDefaultSet && (propUnchanged || propIsCustom)) + || (isDefaultSet && !propIsOverriden) + ) { + if (menu.length > 0) { + menu.push('-'); + } + menu.push({ + text: _('property_remove'), + scope: this, + handler: this.remove.createDelegate(this, [{ + title: _('warning'), + text: _('property_remove_confirm') + }]) + }); + } } + return menu; + }, - return m; - } - - ,propertyChanged: function() { - var ep = Ext.getCmp(this.config.panel); - if (!ep) return false; - var hf = this.config.hiddenPropField || 'props'; - ep.getForm().findField(hf).setValue('1'); - ep.fireEvent('fieldChange',{ - field: hf - ,form: ep.getForm() + /** + * Updates hidden field with the current set of serialized properties to + * be persisted to the database. Only applies to an Element's editing panel + * (in its Properties tab), not to the standalone Property Sets editor. + */ + propertyChanged: function() { + const elementPanel = Ext.getCmp(this.config.panel); + if (!elementPanel) { + return false; + } + const propsValueField = this.config.hiddenPropField || 'props'; + elementPanel.getForm().findField(propsValueField).setValue('1'); + elementPanel.fireEvent('fieldChange', { + field: propsValueField, + form: elementPanel.getForm() }); return true; } }); -Ext.reg('modx-grid-element-properties',MODx.grid.ElementProperties); - +Ext.reg('modx-grid-element-properties', MODx.grid.ElementProperties); -MODx.grid.ElementPropertyOption = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('property_options') - ,id: 'modx-grid-element-property-options' - ,autoHeight: true - ,maxHeight: 300 - ,width: '100%' - ,fields: ['text','value','name'] - ,data: [] - ,columns: [{ - header: _('name') - ,dataIndex: 'text' - ,width: 150 - ,editor: { xtype: 'textfield' ,allowBlank: false } - },{ - header: _('value') - ,dataIndex: 'value' - ,id: 'value' - ,width: 250 - ,editor: { xtype: 'textfield' ,allowBlank: true } - }] - ,tbar: [{ - text: _('property_option_create') - ,cls: 'primary-button' - ,handler: this.create - ,scope: this +MODx.grid.ElementPropertyOption = function(config = {}) { + Ext.applyIf(config, { + title: _('property_options'), + id: 'modx-grid-element-property-options', + autoHeight: true, + maxHeight: 300, + width: '100%', + fields: [ + 'text', + 'value', + 'name' + ], + data: [], + columns: [{ + header: _('name'), + dataIndex: 'text', + width: 150, + editor: { + xtype: 'textfield', + allowBlank: false + } + }, { + header: _('value'), + dataIndex: 'value', + id: 'value', + width: 250, + editor: { + xtype: 'textfield' + } + }], + tbar: [{ + text: _('property_option_create'), + cls: 'primary-button', + handler: this.create, + scope: this }] }); - MODx.grid.ElementPropertyOption.superclass.constructor.call(this,config); - this.optRecord = Ext.data.Record.create([{name: 'text'},{name: 'value'}]); + MODx.grid.ElementPropertyOption.superclass.constructor.call(this, config); + this.optRecord = Ext.data.Record.create([ + { name: 'text' }, + { name: 'value' } + ]); }; -Ext.extend(MODx.grid.ElementPropertyOption,MODx.grid.LocalGrid,{ - create: function(btn,e) { - this.loadWindow(btn,e,{ - xtype: 'modx-window-element-property-option-create' - ,listeners: { - 'success': {fn:function(r) { - var rec = new this.optRecord({ - text: r.text - ,value: r.value - }); - this.getStore().add(rec); - },scope:this} +Ext.extend(MODx.grid.ElementPropertyOption, MODx.grid.LocalGrid, { + create: function(btn, e) { + this.loadWindow(btn, e, { + xtype: 'modx-window-element-property-option-create', + listeners: { + success: { + fn: function(response) { + const record = new this.optRecord({ + text: response.text, + value: response.value + }); + this.getStore().add(record); + }, + scope: this + } } }); - } + }, - ,getMenu: function() { + getMenu: function() { return [{ - text: _('property_option_remove') - ,scope: this - ,handler: this.remove.createDelegate(this,[{ - title: _('warning') - ,text: _('property_option_remove_confirm') + text: _('property_option_remove'), + scope: this, + handler: this.remove.createDelegate(this, [{ + title: _('warning'), + text: _('property_option_remove_confirm') }]) }]; } }); -Ext.reg('modx-grid-element-property-options',MODx.grid.ElementPropertyOption); +Ext.reg('modx-grid-element-property-options', MODx.grid.ElementPropertyOption); /** * @class MODx.window.CreateElementProperty @@ -602,322 +685,190 @@ Ext.reg('modx-grid-element-property-options',MODx.grid.ElementPropertyOption); * @param {Object} config An object of configuration properties * @xtype modx-window-element-property-create */ -MODx.window.CreateElementProperty = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('property_create') - ,id: 'modx-window-element-property-create' - ,width: 600 - ,saveBtnText: _('done') - ,fields: [{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,anchor: '100%' - ,border: false - } - ,items: [{ - columnWidth: .6 - ,items: [{ - fieldLabel: _('name') - ,description: MODx.expandHelp ? '' : _('property_name_desc') - ,name: 'name' - ,id: 'modx-cep-name' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: false - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-cep-name' - ,html: _('property_name_desc') - ,cls: 'desc-under' - },{ - fieldLabel: _('description') - ,description: MODx.expandHelp ? '' : _('property_description_desc') - ,name: 'desc' - ,id: 'modx-cep-desc' - ,xtype: 'textarea' - ,anchor: '100%' - ,height: 120 - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-cep-description' - ,html: _('property_description_desc') - ,cls: 'desc-under' +MODx.window.CreateElementProperty = function(config = {}) { + const + id = Ext.id(), + action = config.isUpdate ? 'update' : 'create' + ; + this.id = `modx-window-${action}-property-${id}`; + Ext.applyIf(config, { + title: _('property_create'), + width: 600, + saveBtnText: _('done'), + fields: [{ + layout: 'column', + border: false, + defaults: { + layout: 'form', + labelAlign: 'top', + anchor: '100%', + border: false + }, + items: [{ + columnWidth: 0.6, + items: [{ + fieldLabel: _('name'), + description: MODx.expandHelp ? '' : _('property_name_desc'), + name: 'name', + xtype: 'textfield', + anchor: '100%', + allowBlank: false + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('property_name_desc'), + cls: 'desc-under' + }, { + fieldLabel: _('description'), + description: MODx.expandHelp ? '' : _('property_description_desc'), + name: 'desc', + xtype: 'textarea', + anchor: '100%', + height: 120 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('property_description_desc'), + cls: 'desc-under' }] - },{ - columnWidth: .4 - ,items: [{ - fieldLabel: _('type') - ,description: MODx.expandHelp ? '' : _('property_xtype_desc') - ,name: 'xtype' - ,id: 'modx-cep-xtype' - ,xtype: 'modx-combo-xtype' - ,anchor: '100%' - ,listeners: { - 'select': {fn:function(cb) { - var g = Ext.getCmp('modx-cep-grid-element-property-options'); - if (!g) return; - if (cb.getValue() == 'list' || cb.getValue() == 'color') { - g.show(); - } else { - g.hide(); - } - this.syncSize(); - },scope:this} + }, { + columnWidth: 0.4, + items: [{ + fieldLabel: _('type'), + description: MODx.expandHelp ? '' : _('property_xtype_desc'), + name: 'xtype', + id: `modx-property-xtype--${this.id}`, + xtype: 'modx-combo-xtype', + anchor: '100%', + listeners: { + select: { + fn: function(combo) { + const optsGrid = Ext.getCmp(`modx-grid--property-options--${this.id}`); + if (!optsGrid) { + return; + } + if (['list', 'color'].includes(combo.getValue())) { + optsGrid.show(); + } else { + optsGrid.hide(); + } + this.syncSize(); + }, + scope: this + } } - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-cep-xtype' - ,html: _('property_xtype_desc') - ,cls: 'desc-under' - },{ - xtype: 'textfield' - ,fieldLabel: _('lexicon') - ,description: MODx.expandHelp ? '' : _('property_lexicon_desc') - ,name: 'lexicon' - ,id: 'modx-cep-lexicon' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-cep-lexicon' - ,html: _('property_lexicon_desc') - ,cls: 'desc-under' - },{ - xtype: 'textfield' - ,fieldLabel: _('area') - ,description: MODx.expandHelp ? '' : _('property_area_desc') - ,name: 'area' - ,id: 'modx-cep-area' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-cep-area' - ,html: _('property_area_desc') - ,cls: 'desc-under' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('property_xtype_desc'), + cls: 'desc-under' + }, { + xtype: 'textfield', + fieldLabel: _('lexicon'), + description: MODx.expandHelp ? '' : _('property_lexicon_desc'), + name: 'lexicon', + anchor: '100%', + allowBlank: true + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('property_lexicon_desc'), + cls: 'desc-under' + }, { + xtype: 'textfield', + fieldLabel: _('area'), + description: MODx.expandHelp ? '' : _('property_area_desc'), + name: 'area', + anchor: '100%', + allowBlank: true + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('property_area_desc'), + cls: 'desc-under' }] }] - },{ - xtype: 'modx-element-value-field' - ,xtypeField: 'modx-cep-xtype' - ,id: 'modx-cep-value' - ,anchor: '100%' - },{ - xtype: 'modx-grid-element-property-options' - ,id: 'modx-cep-grid-element-property-options' - ,anchor: '100%' - }] - ,keys: [] + }, { + xtype: 'modx-element-value-field', + xtypeField: `modx-property-xtype--${this.id}`, + anchor: '100%' + }, { + xtype: 'modx-grid-element-property-options', + id: `modx-grid--property-options--${this.id}`, + anchor: '100%' + }], + keys: [] }); - MODx.window.CreateElementProperty.superclass.constructor.call(this,config); - this.on('show',this.onShow,this); + MODx.window.CreateElementProperty.superclass.constructor.call(this, config); + this.on('show', this.onShow, this); }; -Ext.extend(MODx.window.CreateElementProperty,MODx.Window,{ +Ext.extend(MODx.window.CreateElementProperty, MODx.Window, { submit: function() { - var v = this.fp.getForm().getValues(); - - var g = Ext.getCmp('modx-cep-grid-element-property-options'); - var opt = eval(g.encode()); - Ext.apply(v,{ - options: opt + const + values = this.fp.getForm().getValues(), + optsGrid = Ext.getCmp(`modx-grid--property-options--${this.id}`), + // eslint-disable-next-line no-eval + options = eval(optsGrid.encode()) + ; + Ext.apply(values, { + options: options }); - if (this.fp.getForm().isValid()) { - if (this.fireEvent('success',v)) { + if (this.fireEvent('success', values)) { this.fp.getForm().reset(); this.hide(); return true; } } return false; - } - ,onShow: function() { - var g = Ext.getCmp('modx-cep-grid-element-property-options'); - g.getStore().removeAll(); - g.hide(); + }, + + onShow: function() { + const optsGrid = Ext.getCmp(`modx-grid--property-options--${this.id}`); + if (!optsGrid) { + return; + } + optsGrid.getStore().removeAll(); + optsGrid.hide(); + if ( + this.config.isUpdate + && ['list', 'color'].includes(this.fp.getForm().findField('xtype').getValue()) + ) { + const + propsGrid = Ext.getCmp('modx-grid-element-properties'), + selectedRecord = propsGrid.getSelectionModel().getSelected() + ; + if (selectedRecord) { + const + { options } = selectedRecord.data, + optionsData = [] + ; + options.forEach(option => optionsData.push([option.text, option.value])); + optsGrid.getStore().loadData(optionsData); + optsGrid.show(); + } + } this.syncSize(); this.center(); } }); -Ext.reg('modx-window-element-property-create',MODx.window.CreateElementProperty); - - +Ext.reg('modx-window-element-property-create', MODx.window.CreateElementProperty); /** * @class MODx.window.UpdateElementProperty - * @extends MODx.Window + * @extends MODx.window.CreateElementProperty * @param {Object} config An object of configuration properties * @xtype modx-window-element-property-update */ -MODx.window.UpdateElementProperty = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('property_update') - ,id: 'modx-window-element-property-update' - ,width: 600 - ,saveBtnText: _('done') - ,forceLayout: true - ,fields: [{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,anchor: '100%' - ,border: false - } - ,items: [{ - columnWidth: .6 - ,items: [{ - fieldLabel: _('name') - ,description: MODx.expandHelp ? '' : _('property_name_desc') - ,name: 'name' - ,id: 'modx-uep-name' - ,xtype: 'textfield' - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-uep-name' - ,html: _('property_name_desc') - ,cls: 'desc-under' - },{ - fieldLabel: _('description') - ,description: MODx.expandHelp ? '' : _('property_description_desc') - ,name: 'desc' - ,id: 'modx-uep-desc' - ,xtype: 'textarea' - ,anchor: '100%' - ,height: 120 - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-uep-description' - ,html: _('property_description_desc') - ,cls: 'desc-under' - }] - },{ - columnWidth: .4 - ,items: [{ - fieldLabel: _('type') - ,description: MODx.expandHelp ? '' : _('property_xtype_desc') - ,name: 'xtype' - ,xtype: 'modx-combo-xtype' - ,id: 'modx-uep-xtype' - ,anchor: '100%' - ,listeners: { - 'select': {fn:function(cb) { - var g = Ext.getCmp('modx-uep-grid-element-property-options'); - if (!g) return; - var v = cb.getValue(); - if (v == 'list' || v == 'color') { - g.show(); - } else { - g.hide(); - } - this.syncSize(); - },scope:this} - } - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-uep-xtype' - ,html: _('property_xtype_desc') - ,cls: 'desc-under' - },{ - xtype: 'textfield' - ,fieldLabel: _('lexicon') - ,description: MODx.expandHelp ? '' : _('property_lexicon_desc') - ,name: 'lexicon' - ,id: 'modx-uep-lexicon' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-uep-lexicon' - ,html: _('property_lexicon_desc') - ,cls: 'desc-under' - },{ - xtype: 'textfield' - ,fieldLabel: _('area') - ,description: MODx.expandHelp ? '' : _('property_area_desc') - ,name: 'area' - ,id: 'modx-uep-area' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-uep-area' - ,html: _('property_area_desc') - ,cls: 'desc-under' - }] - }] - },{ - xtype: 'hidden' - ,name: 'overridden' - ,id: 'modx-uep-overridden' - },{ - xtype: 'modx-element-value-field' - ,xtypeField: 'modx-uep-xtype' - ,name: 'value' - ,id: 'modx-uep-value' - ,anchor: '100%' - },{ - id: 'modx-uep-grid-element-property-options' - ,xtype: 'modx-grid-element-property-options' - ,autoHeight: true - }] - ,keys: [] +MODx.window.UpdateElementProperty = function(config = {}) { + Ext.applyIf(config, { + title: _('property_update'), + isUpdate: true }); - MODx.window.UpdateElementProperty.superclass.constructor.call(this,config); - this.on('show',this.onShow,this); + MODx.window.UpdateElementProperty.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.UpdateElementProperty,MODx.Window,{ - submit: function() { - var v = this.fp.getForm().getValues(); - - var g = Ext.getCmp('modx-uep-grid-element-property-options'); - var opt = eval(g.encode()); - Ext.apply(v,{ - options: opt - }); - - if (this.fp.getForm().isValid()) { - if (this.fireEvent('success',v)) { - this.fp.getForm().reset(); - this.hide(); - return true; - } - } - return false; - } - ,onShow: function() { - var g = Ext.getCmp('modx-uep-grid-element-property-options'); - if (!g) return; - if (this.fp.getForm().findField('xtype').getValue() == 'list' || this.fp.getForm().findField('xtype').getValue() == 'color') { - g.show(); - } else { - g.hide(); - } - g.getStore().removeAll(); - var gp = Ext.getCmp('modx-grid-element-properties'); - var rec = gp.getSelectionModel().getSelected(); - if (rec) { - var opt = rec.data.options; - var opts = []; - for (var x in opt) { - if (opt.hasOwnProperty(x)) { - opts.push([opt[x].text,opt[x].value]); - } - } - g.getStore().loadData(opts); - } - this.syncSize(); - this.center(); - } -}); -Ext.reg('modx-window-element-property-update',MODx.window.UpdateElementProperty); +Ext.extend(MODx.window.UpdateElementProperty, MODx.window.CreateElementProperty); +Ext.reg('modx-window-element-property-update', MODx.window.UpdateElementProperty); /** * @class MODx.window.CreateElementPropertyOption @@ -925,32 +876,31 @@ Ext.reg('modx-window-element-property-update',MODx.window.UpdateElementProperty) * @param {Object} config An object of configuration properties * @xtype modx-window-element-property-option-create */ -MODx.window.CreateElementPropertyOption = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('property_option_create') - ,id: 'modx-window-element-property-option-create' - ,saveBtnText: _('done') - ,fields: [{ - fieldLabel: _('name') - ,name: 'text' - ,id: 'modx-cepo-text' - ,xtype: 'textfield' - ,anchor: '100%' - },{ - fieldLabel: _('value') - ,name: 'value' - ,id: 'modx-cepo-value' - ,xtype: 'textfield' - ,anchor: '100%' +MODx.window.CreateElementPropertyOption = function(config = {}) { + Ext.applyIf(config, { + title: _('property_option_create'), + id: 'modx-window-element-property-option-create', + saveBtnText: _('done'), + fields: [{ + fieldLabel: _('name'), + name: 'text', + id: 'modx-cepo-text', + xtype: 'textfield', + anchor: '100%' + }, { + fieldLabel: _('value'), + name: 'value', + id: 'modx-cepo-value', + xtype: 'textfield', + anchor: '100%' }] }); - MODx.window.CreateElementPropertyOption.superclass.constructor.call(this,config); + MODx.window.CreateElementPropertyOption.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateElementPropertyOption,MODx.Window,{ +Ext.extend(MODx.window.CreateElementPropertyOption, MODx.Window, { submit: function() { if (this.fp.getForm().isValid()) { - if (this.fireEvent('success',this.fp.getForm().getValues())) { + if (this.fireEvent('success', this.fp.getForm().getValues())) { this.fp.getForm().reset(); this.hide(); return true; @@ -959,9 +909,7 @@ Ext.extend(MODx.window.CreateElementPropertyOption,MODx.Window,{ return false; } }); -Ext.reg('modx-window-element-property-option-create',MODx.window.CreateElementPropertyOption); - - +Ext.reg('modx-window-element-property-option-create', MODx.window.CreateElementPropertyOption); /** * Displays a xtype combobox @@ -971,84 +919,77 @@ Ext.reg('modx-window-element-property-option-create',MODx.window.CreateElementPr * @param {Object} config An object of configuration properties * @xtype modx-combo-xtype */ -MODx.combo.xType = function(config) { - config = config || {}; - Ext.applyIf(config,{ +MODx.combo.xType = function(config = {}) { + Ext.applyIf(config, { store: new Ext.data.SimpleStore({ - fields: ['d','v'] - ,data: [ - [_('textfield'),'textfield'] - ,[_('textarea'),'textarea'] - ,[_('yesno'),'combo-boolean'] - ,[_('date'),'datefield'] - ,[_('list'),'list'] - ,[_('integer'),'numberfield'] - ,[_('file'),'file'] - ,[_('color'),'color'] + fields: ['d', 'v'], + data: [ + [_('textfield'), 'textfield'], + [_('textarea'), 'textarea'], + [_('yesno'), 'combo-boolean'], + [_('date'), 'datefield'], + [_('list'), 'list'], + [_('integer'), 'numberfield'], + [_('file'), 'file'], + [_('color'), 'color'] ] - }) - ,displayField: 'd' - ,valueField: 'v' - ,mode: 'local' - ,name: 'xtype' - ,hiddenName: 'xtype' - ,triggerAction: 'all' - ,editable: false - ,selectOnFocus: false - ,value: 'textfield' + }), + displayField: 'd', + valueField: 'v', + mode: 'local', + name: 'xtype', + hiddenName: 'xtype', + triggerAction: 'all', + editable: false, + selectOnFocus: false, + value: 'textfield' }); - MODx.combo.xType.superclass.constructor.call(this,config); + MODx.combo.xType.superclass.constructor.call(this, config); }; -Ext.extend(MODx.combo.xType,Ext.form.ComboBox); -Ext.reg('modx-combo-xtype',MODx.combo.xType); - - - +Ext.extend(MODx.combo.xType, Ext.form.ComboBox); +Ext.reg('modx-combo-xtype', MODx.combo.xType); -MODx.form.ElementValueField = function(config) { - config = config || {}; - Ext.applyIf(config,{ - fieldLabel: _('value') - ,name: 'value' - ,xtype: 'textfield' +MODx.form.ElementValueField = function(config = {}) { + Ext.applyIf(config, { + fieldLabel: _('value'), + name: 'value', + xtype: 'textfield' }); - MODx.form.ElementValueField.superclass.constructor.call(this,config); + MODx.form.ElementValueField.superclass.constructor.call(this, config); this.config = config; - this.on('change',this.checkValue,this); + this.on('change', this.checkValue, this); }; -Ext.extend(MODx.form.ElementValueField,Ext.form.TextField,{ - checkValue: function(fld,nv,ov) { - var t = Ext.getCmp(this.config.xtypeField).getValue(); - var v = fld.getValue(); - if (t == 'combo-boolean') { - v = (v == '1' || v == 'true' || v == 1 || v == true || v == _('yes') || v == 'yes') ? 1 : 0; - fld.setValue(v); +Ext.extend(MODx.form.ElementValueField, Ext.form.TextField, { + checkValue: function(field, newValue, oldValue) { + const xtype = Ext.getCmp(this.config.xtypeField).getValue(); + if (xtype === 'combo-boolean') { + let value = field.getValue(); + value = [1, '1', true, 'true', _('yes'), 'yes'].includes(value) ? 1 : 0; + field.setValue(value); } } }); -Ext.reg('modx-element-value-field',MODx.form.ElementValueField); +Ext.reg('modx-element-value-field', MODx.form.ElementValueField); - -MODx.combo.PropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - name: 'propertyset' - ,hiddenName: 'propertyset' - ,url: MODx.config.connector_url - ,baseParams: { +MODx.combo.PropertySet = function(config = {}) { + Ext.applyIf(config, { + name: 'propertyset', + hiddenName: 'propertyset', + url: MODx.config.connector_url, + baseParams: { action: 'Element/PropertySet/GetList' - } - ,displayField: 'name' - ,valueField: 'id' - ,fields: ['id','name','description','properties'] - ,editable: false - ,value: 0 - ,pageSize: 10 + }, + displayField: 'name', + valueField: 'id', + fields: ['id', 'name', 'description', 'properties'], + editable: false, + value: 0, + pageSize: 10 }); - MODx.combo.PropertySet.superclass.constructor.call(this,config); + MODx.combo.PropertySet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.combo.PropertySet,MODx.combo.ComboBox); -Ext.reg('modx-combo-property-set',MODx.combo.PropertySet); +Ext.extend(MODx.combo.PropertySet, MODx.combo.ComboBox); +Ext.reg('modx-combo-property-set', MODx.combo.PropertySet); /** * @class MODx.window.AddPropertySet @@ -1056,115 +997,115 @@ Ext.reg('modx-combo-property-set',MODx.combo.PropertySet); * @param {Object} config An object of configuration properties * @xtype modx-window-element-property-set-add */ -MODx.window.AddPropertySet = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('propertyset_add') - ,id: 'modx-window-element-property-set-add' - ,url: MODx.config.connector_url - ,action: 'Element/PropertySet/Associate' - ,autoHeight: true // makes window grow when the fieldset is toggled - ,fields: [{ - xtype: 'hidden' - ,name: 'elementId' - ,id: 'modx-aps-elementId' - },{ - xtype: 'hidden' - ,name: 'elementType' - ,id: 'modx-aps-elementType' - },{ - html: _('propertyset_panel_desc') - ,xtype: 'modx-description' +MODx.window.AddPropertySet = function(config = {}) { + Ext.applyIf(config, { + title: _('propertyset_add'), + id: 'modx-window-element-property-set-add', + url: MODx.config.connector_url, + action: 'Element/PropertySet/Associate', + autoHeight: true, // makes window grow when the fieldset is toggled + fields: [{ + xtype: 'hidden', + name: 'elementId' + }, { + xtype: 'hidden', + name: 'elementType' + }, { + html: _('propertyset_panel_desc'), + xtype: 'modx-description' - },MODx.PanelSpacer,{ - xtype: 'modx-combo-property-set' - ,fieldLabel: _('propertyset') - ,name: 'propertyset' - ,id: 'modx-aps-propertyset' - ,anchor: '100%' - ,baseParams: { - action: 'Element/PropertySet/GetList' - ,showNotAssociated: true - ,elementId: config.record.elementId - ,elementType: config.record.elementType + }, MODx.PanelSpacer, { + xtype: 'modx-combo-property-set', + fieldLabel: _('propertyset'), + name: 'propertyset', + anchor: '100%', + baseParams: { + action: 'Element/PropertySet/GetList', + showNotAssociated: true, + elementId: config.record.elementId, + elementType: config.record.elementType } - },{ - xtype: 'hidden' - ,name: 'propertyset_new' - ,id: 'modx-aps-propertyset-new' - ,value: false - },{ - xtype: 'fieldset' - ,title: _('propertyset_create_new') - ,autoHeight: true - ,checkboxToggle: true - ,collapsed: true - ,forceLayout: true - ,id: 'modx-aps-propertyset-new-fs' - ,listeners: { - 'expand': {fn:function(p) { - Ext.getCmp('modx-aps-propertyset-new').setValue(true); - this.center(); // re-centers window on screen after height changed - },scope:this} - ,'collapse': {fn:function(p) { - Ext.getCmp('modx-aps-propertyset-new').setValue(false); - this.center(); // re-centers window on screen after height changed - },scope:this} - } - ,items: [{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'name' - ,id: 'modx-aps-name' - ,anchor: '100%' - },{ - xtype: 'textarea' - ,fieldLabel: _('description') - ,name: 'description' - ,id: 'modx-aps-description' - ,anchor: '100%' - ,grow: true + }, { + xtype: 'hidden', + name: 'propertyset_new', + id: 'modx-aps-propertyset-new', + value: false + }, { + xtype: 'fieldset', + title: _('propertyset_create_new'), + autoHeight: true, + checkboxToggle: true, + collapsed: true, + forceLayout: true, + listeners: { + expand: { + fn: function(p) { + Ext.getCmp('modx-aps-propertyset-new').setValue(true); + this.center(); + }, + scope: this + }, + collapse: { + fn: function(p) { + Ext.getCmp('modx-aps-propertyset-new').setValue(false); + this.center(); + }, + scope: this + } + }, + items: [{ + xtype: 'textfield', + fieldLabel: _('name'), + name: 'name', + anchor: '100%' + }, { + xtype: 'textarea', + fieldLabel: _('description'), + name: 'description', + anchor: '100%', + grow: true }] }] }); - MODx.window.AddPropertySet.superclass.constructor.call(this,config); + MODx.window.AddPropertySet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.AddPropertySet,MODx.Window); -Ext.reg('modx-window-element-property-set-add',MODx.window.AddPropertySet); +Ext.extend(MODx.window.AddPropertySet, MODx.Window); +Ext.reg('modx-window-element-property-set-add', MODx.window.AddPropertySet); -MODx.window.ImportProperties = function(config) { - config = config || {}; - Ext.applyIf(config,{ - title: _('import') - ,id: 'modx-window-properties-import' - ,url: MODx.config.connector_url - ,action: 'Element/ImportProperties' - ,fileUpload: true - ,saveBtnText: _('import') - ,fields: [{ - html: _('properties_import_msg') - ,id: 'modx-impp-desc' - ,style: 'margin-bottom: 10px;' - ,xtype: 'modx-description' - },{ - xtype: 'fileuploadfield' - ,fieldLabel: _('file') - ,buttonText: _('upload.buttons.upload') - ,name: 'file' - ,id: 'modx-impp-file' - ,anchor: '100%' +MODx.window.ImportProperties = function(config = {}) { + Ext.applyIf(config, { + title: _('import'), + id: 'modx-window-properties-import', + url: MODx.config.connector_url, + action: 'Element/ImportProperties', + fileUpload: true, + saveBtnText: _('import'), + fields: [{ + html: _('properties_import_msg'), + style: 'margin-bottom: 10px;', + xtype: 'modx-description' + }, { + xtype: 'fileuploadfield', + fieldLabel: _('file'), + buttonText: _('upload.buttons.upload'), + name: 'file', + id: 'modx-impp-file', + anchor: '100%' }] }); - MODx.window.ImportProperties.superclass.constructor.call(this,config); - - // Trigger "fileselected" event - var fp = Ext.getCmp('modx-impp-file'); - var onFileUploadFieldFileSelected = function(fp, fakeFilePath) { - var fileApi = fp.fileInput.dom.files; - fp.el.dom.value = (typeof fileApi != 'undefined') ? fileApi[0].name : fakeFilePath.replace("C:\\fakepath\\", ""); - }; - fp.on('fileselected', onFileUploadFieldFileSelected); + MODx.window.ImportProperties.superclass.constructor.call(this, config); + const + fileCmp = Ext.getCmp('modx-impp-file'), + onFileUploadFieldFileSelected = function(fileCmp, fakeFilePath) { + const fileApi = fileCmp.fileInput.dom.files; + fileCmp.el.dom.value = (typeof fileApi != 'undefined') + ? fileApi[0].name + : fakeFilePath.replace('C:\\fakepath\\', '') + ; + } + ; + fileCmp.on('fileselected', onFileUploadFieldFileSelected); }; -Ext.extend(MODx.window.ImportProperties,MODx.Window); -Ext.reg('modx-window-properties-import',MODx.window.ImportProperties); +Ext.extend(MODx.window.ImportProperties, MODx.Window); +Ext.reg('modx-window-properties-import', MODx.window.ImportProperties); From f7d1bf9b08e100e8dc739c860629c18dfb6096ea Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Tue, 26 Nov 2024 23:05:00 -0500 Subject: [PATCH 35/39] Update modx.grid.local.property.js Formatting, code style & optimization changes only --- .../widgets/core/modx.grid.local.property.js | 209 ++++++++++-------- 1 file changed, 115 insertions(+), 94 deletions(-) diff --git a/manager/assets/modext/widgets/core/modx.grid.local.property.js b/manager/assets/modext/widgets/core/modx.grid.local.property.js index 9b5b7a3235..eea5f2df70 100644 --- a/manager/assets/modext/widgets/core/modx.grid.local.property.js +++ b/manager/assets/modext/widgets/core/modx.grid.local.property.js @@ -1,119 +1,140 @@ -MODx.grid.LocalProperty = function(config) { - config = config || {}; - Ext.applyIf(config,{ - dynProperty: 'xtype' - ,dynField: 'value' - ,propertyRecord: [{name: 'name'},{name: 'value'}] - ,data: [] +MODx.grid.LocalProperty = function(config = {}) { + Ext.applyIf(config, { + dynProperty: 'xtype', + dynField: 'value', + propertyRecord: [ + { name: 'name' }, + { name: 'value' } + ], + data: [] }); - MODx.grid.LocalProperty.superclass.constructor.call(this,config); + MODx.grid.LocalProperty.superclass.constructor.call(this, config); this.propRecord = Ext.data.Record.create(config.propertyRecord); }; -Ext.extend(MODx.grid.LocalProperty,MODx.grid.LocalGrid,{ - onCellDblClick: function(g,ri,ci,e) { - var cm = this.getColumnModel(); - if (cm.getColumnId(ci) == this.config.dynField) { +Ext.extend(MODx.grid.LocalProperty, MODx.grid.LocalGrid, { + onCellDblClick: function(grid, rowIndex, colIndex, e) { + const colModel = this.getColumnModel(); + if (colModel.getColumnId(colIndex) === this.config.dynField) { e.preventDefault(); - var r = this.getStore().getAt(ri).data; - this.initEditor(cm,ci,ri,r); - this.startEditing(ri,ci); + const record = this.getStore().getAt(rowIndex).data; + this.initEditor(colModel, colIndex, rowIndex, record); + this.startEditing(rowIndex, colIndex); } - } + }, - ,initEditor: function(cm,ci,ri,r) { - cm.setEditable(ci,true); - var xtype = this.config.dynProperty; - var o; - if (r[xtype] == 'list') { - o = this.createCombo(r); + initEditor: function(colModel, colIndex, rowIndex, record) { + colModel.setEditable(colIndex, true); + const fieldType = record[this.config.dynProperty]; + let fieldCmp; + if (fieldType === 'list') { + fieldCmp = this.createCombo(record); } else { - var z = {}; - z[xtype] = r[xtype] || 'textfield'; + const config = {}; + config[this.config.dynProperty] = fieldType || 'textfield'; try { - o = Ext.ComponentMgr.create(z); + fieldCmp = Ext.ComponentMgr.create(config); } catch (e) { - z[xtype] = 'textfield'; - o = MODx.load(z); + config[this.config.dynProperty] = 'textfield'; + fieldCmp = MODx.load(config); } } - var ed = new Ext.grid.GridEditor(o); - cm.setEditor(ci,ed); - return ed; - } + const editor = new Ext.grid.GridEditor(fieldCmp); + colModel.setEditor(colIndex, editor); + return editor; + }, + + renderDynField: function(value, metaData, record, rowIndex, colIndex, store, grid) { + const + { data } = record, + fieldType = data[this.config.dynProperty], + encodedValue = Ext.util.Format.htmlEncode(value), + rendererArgs = [encodedValue, metaData, record, rowIndex, colIndex, store, grid] + ; + let renderFn; - ,renderDynField: function(v,md,rec,ri,ci,s,g) { - var r = s.getAt(ri).data; - var f,idx; - var oz = v; - var xtype = this.config.dynProperty; - if (!r[xtype] || r[xtype] == 'combo-boolean') { - f = MODx.grid.Grid.prototype.rendYesNo; - return this.renderEditableColumn(f)(Ext.util.Format.htmlEncode(v),md,rec,ri,ci,s,g); - } else if (r[xtype] === 'datefield') { - f = Ext.util.Format.dateRenderer('Y-m-d'); - return this.renderEditableColumn(f)(Ext.util.Format.htmlEncode(v),md,rec,ri,ci,s,g); - } else if (r[xtype] === 'password') { - f = this.rendPassword; - return this.renderEditableColumn(f)(Ext.util.Format.htmlEncode(v),md,rec,ri,ci,s,g); - } else if (r[xtype].substr(0,5) == 'combo' || r[xtype] == 'list' || r[xtype].substr(0,9) == 'modx-combo') { - var cm = g.getColumnModel(); - var ed = cm.getCellEditor(ci,ri); - var cb; - if (!ed) { - r.xtype = r.xtype || 'combo-boolean'; - cb = this.createCombo(r); - ed = new Ext.grid.GridEditor(cb); - cm.setEditor(ci,ed); - } else if (ed && ed.field && ed.field.xtype == 'modx-combo') { - cb = ed.field; + if (!fieldType || fieldType === 'combo-boolean') { + renderFn = MODx.grid.Grid.prototype.rendYesNo; + return this.renderEditableColumn(renderFn)(...rendererArgs); + } + if (fieldType === 'datefield') { + renderFn = Ext.util.Format.dateRenderer('Y-m-d'); + return this.renderEditableColumn(renderFn)(...rendererArgs); + } + if (fieldType === 'password') { + renderFn = this.rendPassword; + return this.renderEditableColumn(renderFn)(...rendererArgs); + } + if (fieldType.includes('combo') || fieldType === 'list') { + const colModel = grid.getColumnModel(); + let + editor = colModel.getCellEditor(colIndex, rowIndex), + comboCmp + ; + if (!editor) { + data.xtype = data.xtype || 'combo-boolean'; + comboCmp = this.createCombo(data); + editor = new Ext.grid.GridEditor(comboCmp); + colModel.setEditor(colIndex, editor); + } else if (editor?.field?.xtype === 'modx-combo') { + comboCmp = editor.field; } - if (r[xtype] != 'list') { - f = Ext.util.Format.comboRenderer(ed.field); - return this.renderEditableColumn(f)(Ext.util.Format.htmlEncode(v),md,rec,ri,ci,s,g); - } else if (cb) { - idx = cb.getStore().find(cb.valueField,v); - rec = cb.getStore().getAt(idx); - if (rec) { - oz = rec.get(cb.displayField); - } else { - oz = v; + if (fieldType !== 'list') { + renderFn = Ext.util.Format.comboRenderer(editor.field); + return this.renderEditableColumn(renderFn)(...rendererArgs); + } + if (comboCmp) { + const + valueIndex = comboCmp.getStore().find(comboCmp.valueField, value), + comboRecord = comboCmp.getStore().getAt(valueIndex) + ; + if (comboRecord) { + const displayValue = comboRecord.get(comboCmp.displayField); + // override args in upper scope with this combo's value and record + rendererArgs[0] = Ext.util.Format.htmlEncode(displayValue); + rendererArgs[2] = comboRecord; } } } + return this.renderEditableColumn()(...rendererArgs); + }, - return this.renderEditableColumn()(Ext.util.Format.htmlEncode(oz),md,rec,ri,ci,s,g); - } - - ,createCombo: function(p) { - var obj; + createCombo: function(record) { + let combo; try { - obj = Ext.ComponentMgr.create({ xtype: r.xtype, id: Ext.id() }); - } catch(e) { + combo = Ext.ComponentMgr.create({ + xtype: record.xtype, + id: Ext.id() + }); + } catch (e) { try { - var flds = p.options; - var data = []; - for (var i=0;i data.push([option.name, option.value, option.text])); + + combo = MODx.load({ + xtype: 'modx-combo', + store: new Ext.data.SimpleStore({ + fields: ['d', 'v', 't'], + data: data + }), + displayField: 'd', + valueField: 'v', + mode: 'local', + triggerAction: 'all', + editable: false, + selectOnFocus: false, + preventRender: true }); } catch (e2) { - obj = Ext.ComponentMgr.create({ xtype: 'combo-boolean', id: Ext.id() }); + combo = Ext.ComponentMgr.create({ + xtype: 'combo-boolean', + id: Ext.id() + }); } } - return obj; + return combo; } }); -Ext.reg('grid-local-property',MODx.grid.LocalProperty); +Ext.reg('grid-local-property', MODx.grid.LocalProperty); From 3d6412b6a813c7d3ecf559dfe5b62e3259a691b1 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 28 Nov 2024 22:34:49 -0500 Subject: [PATCH 36/39] Property Sets updates Updates display of and ability to select row actions (gear icon), as well as display of various action buttons. Also adjustments made to base grid class. --- .../Processors/Element/PropertySet/Get.php | 17 ++- .../Element/PropertySet/GetList.php | 4 +- .../Element/PropertySet/GetNodes.php | 30 ++--- .../assets/modext/widgets/core/modx.grid.js | 50 ++++---- .../widgets/core/modx.grid.local.property.js | 2 + .../element/modx.grid.element.properties.js | 114 +++++++++++++----- .../element/modx.panel.property.set.js | 6 +- 7 files changed, 151 insertions(+), 72 deletions(-) diff --git a/core/src/Revolution/Processors/Element/PropertySet/Get.php b/core/src/Revolution/Processors/Element/PropertySet/Get.php index 7b04a021a7..6413714143 100644 --- a/core/src/Revolution/Processors/Element/PropertySet/Get.php +++ b/core/src/Revolution/Processors/Element/PropertySet/Get.php @@ -1,4 +1,5 @@ default = $this->getDefaultSet(); $id = $this->getProperty($this->primaryKeyField); + + $canSave = $this->modx->hasPermission('save_propertyset'); + $this->canCreate = $canSave && $this->modx->hasPermission('new_propertyset'); + $this->canEdit = $canSave && $this->modx->hasPermission('edit_propertyset'); + $this->canRemove = $this->modx->hasPermission('delete_propertyset'); + if ($id == 0) { if (empty($this->default)) { return $this->modx->lexicon($this->objectType . '_err_nfs', ['id' => $id]); @@ -158,6 +168,11 @@ public function setData(array $properties, array &$data, $isDefault = false) $property['desc_trans'], !empty($property['area']) ? $property['area'] : '', !empty($property['area_trans']) ? $property['area_trans'] : ($isDefault ? '' : $property['area']), + [ + 'create' => $this->canCreate, + 'update' => $this->canEdit, + 'delete' => $this->canRemove + ] ]; } } diff --git a/core/src/Revolution/Processors/Element/PropertySet/GetList.php b/core/src/Revolution/Processors/Element/PropertySet/GetList.php index ee2ce443cb..3bed14e4e1 100644 --- a/core/src/Revolution/Processors/Element/PropertySet/GetList.php +++ b/core/src/Revolution/Processors/Element/PropertySet/GetList.php @@ -1,4 +1,5 @@ node = explode('_', $id); - /* check permissions */ - $this->has = [ - 'save' => $this->modx->hasPermission('save_propertyset'), - 'remove' => $this->modx->hasPermission('delete_propertyset'), - 'new' => $this->modx->hasPermission('new_propertyset'), - ]; + $this->canCreate = $this->modx->hasPermission('save_propertyset') && $this->modx->hasPermission('new_propertyset'); + $this->canEdit = $this->modx->hasPermission('save_propertyset') && $this->modx->hasPermission('edit_propertyset'); + $this->canRemove = $this->modx->hasPermission('delete_propertyset'); return true; } @@ -65,7 +64,7 @@ public function initialize() * * @return string */ - function getNodeIcon($elementIdentifier = '') + public function getNodeIcon($elementIdentifier = '') { $elementIdentifier = strtolower($elementIdentifier); $defaults = [ @@ -134,7 +133,7 @@ public function getCategoryNode($category) /** @var modPropertySet $set */ foreach ($sets as $set) { $menu = []; - if ($this->has['save']) { + if ($this->canEdit) { $menu[] = [ 'text' => $this->modx->lexicon($this->objectType . '_element_add'), 'handler' => 'function(itm,e) { @@ -149,7 +148,7 @@ public function getCategoryNode($category) }', ]; } - if ($this->has['new'] && $this->has['save']) { + if ($this->canCreate) { $menu[] = [ 'text' => $this->modx->lexicon($this->objectType . '_duplicate'), 'handler' => 'function(itm,e) { @@ -157,7 +156,7 @@ public function getCategoryNode($category) }', ]; } - if ($this->has['remove']) { + if ($this->canRemove) { $menu[] = '-'; $menu[] = [ 'text' => $this->modx->lexicon($this->objectType . '_remove'), @@ -218,7 +217,10 @@ public function getPropertySetNode() continue; } $menu = []; - if ($this->has['remove']) { + /* + Note that this action removes (detaches) an Element from a given Property Set, which is really an edit of that Set + */ + if ($this->canEdit) { $menu[] = [ 'text' => $this->modx->lexicon($this->objectType . '_element_remove'), 'handler' => 'function(itm,e) { diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index a16c1fce52..348a268e69 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -161,6 +161,15 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { protectedIdentifiers: null, + /** + * @property {String} permissionsProviderProp Specifies which property within a record contains + * the permissions object ('data' or 'json'). Local grids use Array stores where only the + * data *values* are stored in a simple array (record.json); the permissions and other object + * data must be stored in record.data. Remote stores, however, store their non-form (derived) + * data such as permissions in record.json. + */ + permissionsProviderProp: 'data', + userCanEdit: false, userCanCreate: false, @@ -177,7 +186,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { hasNestedFilters: false, /** @property {Boolean} userHasPermissions Whether user has permissions of any kind to manipulate the current grid's data */ - hasPermissions: false, + userHasPermissions: false, /** @property {Boolean} userHasSavePermissions Whether user has the general ability to save (to either create or edit) */ userHasSavePermissions: false, @@ -373,19 +382,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { if (this.showActionsMenu) { // Export is always available; only continue filtering if grid does not offer export if (!this.gridMenuActions.includes('export')) { - /** - * @var {Object} permissionsDataSource Specifies the property where the record's - * permissions can be found. Local grids use Array stores where only the data *values* - * are stored in a simple array (record.json); the permissions and other object data must - * be stored in record.data. Remote stores, however, store their non-form (derived) data - * such as permissions in record.json. - */ - const - permissionsDataSource = this instanceof MODx.grid.LocalGrid && !(this instanceof MODx.grid.JsonGrid) - ? record.data - : record.json, - isProtected = permissionsDataSource?.isProtected || false - ; + const isProtected = record[this.permissionsProviderProp]?.isProtected || false; if (!this.userHasSavePermissions && isProtected) { return; } @@ -395,10 +392,10 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return; } } - if (Object.hasOwn(permissionsDataSource, 'permissions')) { + if (Object.hasOwn(record[this.permissionsProviderProp], 'permissions')) { if ( - Ext.isEmpty(permissionsDataSource.permissions) - || Object.values(permissionsDataSource.permissions).every(permission => !permission) + Ext.isEmpty(record[this.permissionsProviderProp].permissions) + || Object.values(record[this.permissionsProviderProp].permissions).every(permission => !permission) ) { return; } @@ -705,7 +702,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { // -> Record-Level Permissions Checks, for objects with specific policies userHasRecordPermissions: function(record) { - const objPermissions = record.json.permissions; + const objPermissions = record[this.permissionsProviderProp].permissions; if (Ext.isEmpty(objPermissions)) { return false; } @@ -713,17 +710,17 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { }, userCanEditRecord: function(record) { - const objPermissions = record.json.permissions; + const objPermissions = record[this.permissionsProviderProp].permissions; return !Ext.isEmpty(objPermissions) && objPermissions.update === true; }, userCanDeleteRecord: function(record) { - const objPermissions = record.json.permissions; - return !Ext.isEmpty(objPermissions) && !record.json.isProtected && objPermissions.delete === true; + const objPermissions = record[this.permissionsProviderProp].permissions; + return !Ext.isEmpty(objPermissions) && !record[this.permissionsProviderProp].isProtected && objPermissions.delete === true; }, userCanDuplicateRecord: function(record) { - const objPermissions = record.json.permissions; + const objPermissions = record[this.permissionsProviderProp].permissions; return !Ext.isEmpty(objPermissions) && objPermissions.duplicate === true; }, @@ -1082,6 +1079,10 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return z; }, + /** + * @todo Implement encodeURIComponent to fix issues with passing particluar + * chars via URL (e.g., # [pound/hash] used in color values, etc.) + */ encode: function() { const p = this.getStore().getRange(), rs = {}; @@ -1687,6 +1688,8 @@ MODx.grid.Grid = function(config = {}) { }; Ext.extend(MODx.grid.Grid, MODx.grid.GridBase, { + permissionsProviderProp: 'json', + _loadStore: function() { if (this.config.grouping) { this.store = new Ext.data.GroupingStore({ @@ -2003,6 +2006,8 @@ Ext.extend(MODx.grid.LocalGrid, MODx.grid.GridBase, { /** * @override + * @todo Implement encodeURIComponent to fix issues with passing particluar + * chars via URL (e.g., # [pound/hash] used in color values, etc.) */ encode: function() { const s = this.getStore(), @@ -2012,6 +2017,7 @@ Ext.extend(MODx.grid.LocalGrid, MODx.grid.GridBase, { for (let j = 0; j < ct; j++) { r = s.getAt(j).data; r.menu = null; + r.permissions = null; if (this.config.encodeAssoc) { rs[r[this.config.encodeByPk || 'id']] = r; } else { diff --git a/manager/assets/modext/widgets/core/modx.grid.local.property.js b/manager/assets/modext/widgets/core/modx.grid.local.property.js index eea5f2df70..c06a7f6163 100644 --- a/manager/assets/modext/widgets/core/modx.grid.local.property.js +++ b/manager/assets/modext/widgets/core/modx.grid.local.property.js @@ -52,6 +52,8 @@ Ext.extend(MODx.grid.LocalProperty, MODx.grid.LocalGrid, { ; let renderFn; + metaData.css = this.setEditableCellClasses(record); + if (!fieldType || fieldType === 'combo-boolean') { renderFn = MODx.grid.Grid.prototype.rendYesNo; return this.renderEditableColumn(renderFn)(...rendererArgs); diff --git a/manager/assets/modext/widgets/element/modx.grid.element.properties.js b/manager/assets/modext/widgets/element/modx.grid.element.properties.js index 3064864c3b..dca956a366 100644 --- a/manager/assets/modext/widgets/element/modx.grid.element.properties.js +++ b/manager/assets/modext/widgets/element/modx.grid.element.properties.js @@ -77,7 +77,8 @@ MODx.grid.ElementProperties = function(config = {}) { 'overridden', 'desc_trans', 'area', - 'area_trans' + 'area_trans', + 'permissions' ], autoExpandColumn: 'value', sortBy: 'name', @@ -140,7 +141,8 @@ MODx.grid.ElementProperties = function(config = {}) { action: 'Element/PropertySet/GetList', showAssociated: true, elementId: config.elementId, - elementType: config.elementType + elementType: config.elementType, + combo: true }, value: 0, listeners: { @@ -151,10 +153,12 @@ MODx.grid.ElementProperties = function(config = {}) { } }, { text: _('propertyset_add'), + id: 'modx-btn-property-set-add', handler: this.addPropertySet, scope: this }, { text: _('propertyset_save'), + id: 'modx-btn-property-set-save', cls: 'primary-button', handler: this.save, scope: this, @@ -168,6 +172,7 @@ MODx.grid.ElementProperties = function(config = {}) { disabled: true }, { text: _('import'), + id: 'modx-btn-property-import', handler: this.importProperties, scope: this }, { @@ -190,22 +195,59 @@ MODx.grid.ElementProperties = function(config = {}) { }] }); MODx.grid.ElementProperties.superclass.constructor.call(this, config); - this.on('afteredit', this.propertyChanged, this); - this.on('afterRemoveRow', this.propertyChanged, this); - this.on('render', function() { - this.mask = new Ext.LoadMask(this.getEl()); - }, this); - if (this.config.lockProperties) { - this.on('render', function() { - this.lockMask = MODx.load({ - xtype: 'modx-lockmask', - el: this.getGridEl(), - msg: _('properties_default_locked') - }); - this.lockMask.toggle(); - }, this); - } + // Omitting 'revert' action, as it is effectively the same as 'edit' + this.gridMenuActions = ['edit', 'delete']; + + // Note there are currently no action-specific permissions for Dashboards + this.setUserCanEdit(['edit_propertyset', 'save_propertyset']); + this.setUserCanCreate(['new_propertyset', 'save_propertyset']); + this.setUserCanDelete(['delete_propertyset']); + this.setShowActionsMenu(); + + this.on({ + render: grid => { + const buttonsToHide = []; + this.mask = new Ext.LoadMask(this.getEl()); + if (this.config.lockProperties) { + this.lockMask = MODx.load({ + xtype: 'modx-lockmask', + el: this.getGridEl(), + msg: _('properties_default_locked') + }); + this.lockMask.toggle(); + } + if (!this.userCanCreate) { + buttonsToHide.push('modx-btn-property-set-add', 'modx-btn-property-import'); + } + if (!this.userCanEdit) { + buttonsToHide.push('modx-btn-property-create', 'modx-btn-property-revert-all'); + if (!this.userCanCreate) { + buttonsToHide.push('modx-btn-property-set-save'); + } + } + if ( + !MODx.perm.unlock_element_properties + && !this.id === 'modx-grid-element-properties' + ) { + buttonsToHide.push('modx-btn-propset-lock'); + } + if (buttonsToHide.length > 0) { + buttonsToHide.forEach(btnId => Ext.getCmp(btnId)?.hide()); + } + }, + beforeedit: e => { + if (e.record[this.permissionsProviderProp].isProtected || !this.userCanEditRecord(e.record)) { + return false; + } + }, + afteredit: e => { + this.propertyChanged(); + }, + afterRemoveRow: record => { + this.propertyChanged(); + } + }); }; Ext.extend(MODx.grid.ElementProperties, MODx.grid.LocalProperty, { defaultProperties: [], @@ -496,8 +538,11 @@ Ext.extend(MODx.grid.ElementProperties, MODx.grid.LocalProperty, { }, exportProperties: function(btn, e) { - const propSetId = Ext.getCmp('modx-combo-property-set').getValue(); - window.location.href = `${MODx.config.connector_url}?action=Element/ExportProperties&download=1&id=${propSetId}&data=${this.encode()}&HTTP_MODAUTH=${MODx.siteId}`; + const + propSetId = Ext.getCmp('modx-combo-property-set').getValue(), + data = this.encode() + ; + window.location.href = `${MODx.config.connector_url}?action=Element/ExportProperties&download=1&id=${propSetId}&data=${data}&HTTP_MODAUTH=${MODx.siteId}`; }, importProperties: function(btn, e) { @@ -544,28 +589,31 @@ Ext.extend(MODx.grid.ElementProperties, MODx.grid.LocalProperty, { propUnchanged = [0, false].includes(record.data.overridden), menu = [] ; - if (model.getCount() > 1) { + if (model.getCount() > 1 && this.userCanDelete) { menu.push({ text: _('properties_remove'), handler: this.removeMultiple, scope: this }); } else { - menu.push({ - text: _('property_update'), - scope: this, - handler: this.update - }); - if (propIsOverriden) { + if (this.userCanEdit) { menu.push({ - text: _('property_revert'), + text: _('property_update'), scope: this, - handler: this.revert + handler: this.update }); + if (propIsOverriden) { + menu.push({ + text: _('property_revert'), + scope: this, + handler: this.revert + }); + } } if ( - (!isDefaultSet && (propUnchanged || propIsCustom)) - || (isDefaultSet && !propIsOverriden) + this.userCanDelete + && ((!isDefaultSet && (propUnchanged || propIsCustom)) + || (isDefaultSet && !propIsOverriden)) ) { if (menu.length > 0) { menu.push('-'); @@ -977,7 +1025,8 @@ MODx.combo.PropertySet = function(config = {}) { hiddenName: 'propertyset', url: MODx.config.connector_url, baseParams: { - action: 'Element/PropertySet/GetList' + action: 'Element/PropertySet/GetList', + combo: true }, displayField: 'name', valueField: 'id', @@ -1023,7 +1072,8 @@ MODx.window.AddPropertySet = function(config = {}) { action: 'Element/PropertySet/GetList', showNotAssociated: true, elementId: config.record.elementId, - elementType: config.record.elementType + elementType: config.record.elementType, + combo: true } }, { xtype: 'hidden', diff --git a/manager/assets/modext/widgets/element/modx.panel.property.set.js b/manager/assets/modext/widgets/element/modx.panel.property.set.js index f2a5023af3..0002703bcc 100644 --- a/manager/assets/modext/widgets/element/modx.panel.property.set.js +++ b/manager/assets/modext/widgets/element/modx.panel.property.set.js @@ -72,7 +72,8 @@ MODx.grid.PropertySetProperties = function(config = {}) { xtype: 'modx-combo-property-set', id: 'modx-combo-property-set', baseParams: { - action: 'Element/PropertySet/GetList' + action: 'Element/PropertySet/GetList', + combo: true }, listeners: { select: { @@ -85,6 +86,7 @@ MODx.grid.PropertySetProperties = function(config = {}) { value: '' }, { text: _('property_create'), + id: 'modx-btn-property-create', handler: function(btn, e) { if (Ext.getCmp('modx-combo-property-set').value !== '') { Ext.getCmp('modx-grid-element-properties').create(btn, e); @@ -95,6 +97,7 @@ MODx.grid.PropertySetProperties = function(config = {}) { scope: this }, '->', { text: _('propertyset_save'), + id: 'modx-btn-property-set-save', cls: 'primary-button', handler: function() { Ext.getCmp('modx-grid-element-properties').save(); }, scope: this @@ -125,6 +128,7 @@ MODx.tree.PropertySets = function(config = {}) { text: _('propertyset_new'), cls: 'primary-button', handler: this.createSet, + hidden: !MODx.perm.new_propertyset || !MODx.perm.save_propertyset, scope: this }], useDefaultToolbar: true From fdcc10ba62eadeedcebc46907f5968ec82a7e54b Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 28 Nov 2024 23:26:31 -0500 Subject: [PATCH 37/39] Minor code quality fixes --- .../src/Revolution/Processors/Context/Get.php | 2 +- .../Processors/Security/User/GetList.php | 2 +- core/src/Revolution/modContext.php | 28 +++++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/core/src/Revolution/Processors/Context/Get.php b/core/src/Revolution/Processors/Context/Get.php index 2f57572987..cc5626f407 100644 --- a/core/src/Revolution/Processors/Context/Get.php +++ b/core/src/Revolution/Processors/Context/Get.php @@ -48,4 +48,4 @@ public function beforeOutput() $this->object->set('reserved', $reserved); } } -} \ No newline at end of file +} diff --git a/core/src/Revolution/Processors/Security/User/GetList.php b/core/src/Revolution/Processors/Security/User/GetList.php index a39946610a..af324b86af 100644 --- a/core/src/Revolution/Processors/Security/User/GetList.php +++ b/core/src/Revolution/Processors/Security/User/GetList.php @@ -56,7 +56,7 @@ public function initialize() } $this->canCreate = $this->modx->hasPermission('new_user') && $this->modx->hasPermission('save_user'); - $this->canEdit = $this->modx->hasPermission('edit_user') && $this->modx->hasPermission('save_user');; + $this->canEdit = $this->modx->hasPermission('edit_user') && $this->modx->hasPermission('save_user'); $this->canRemove = $this->modx->hasPermission('delete_user'); return $initialized; diff --git a/core/src/Revolution/modContext.php b/core/src/Revolution/modContext.php index 6454afe109..27eceef2cb 100644 --- a/core/src/Revolution/modContext.php +++ b/core/src/Revolution/modContext.php @@ -128,14 +128,26 @@ public function prepare($regenerate = false, array $options = []) if ($this->config === null || $regenerate) { if ($this->xpdo->getCacheManager()) { $context = []; - if ($regenerate || !($context = $this->xpdo->cacheManager->get($this->getCacheKey(), [ - xPDO::OPT_CACHE_KEY => $this->xpdo->getOption('cache_context_settings_key', null, - 'context_settings'), - xPDO::OPT_CACHE_HANDLER => $this->xpdo->getOption('cache_context_settings_handler', null, - $this->xpdo->getOption(xPDO::OPT_CACHE_HANDLER, null, 'xPDO\Cache\xPDOFileCache')), - xPDO::OPT_CACHE_FORMAT => (int)$this->xpdo->getOption('cache_context_settings_format', null, - $this->xpdo->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)), - ]))) { + if ( + $regenerate || !($context = $this->xpdo->cacheManager->get($this->getCacheKey(), [ + xPDO::OPT_CACHE_KEY => $this->xpdo->getOption( + 'cache_context_settings_key', + null, + 'context_settings' + ), + xPDO::OPT_CACHE_HANDLER => $this->xpdo->getOption( + 'cache_context_settings_handler', + null, + $this->xpdo->getOption(xPDO::OPT_CACHE_HANDLER, null, 'xPDO\Cache\xPDOFileCache') + ), + xPDO::OPT_CACHE_FORMAT => (int)$this->xpdo->getOption( + 'cache_context_settings_format', + null, + $this->xpdo->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP) + ) + ])) + ) { + /** @disregard P1013 Intelephense can not find this modCacheManager instance method, but it does exist and is available here */ $context = $this->xpdo->cacheManager->generateContext($this->get('key'), $options); } if (!empty($context)) { From e1ab50a6a6aa49f9cdc9c8bf2296409ac00ae5a9 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 2 Dec 2024 13:56:35 -0500 Subject: [PATCH 38/39] Update modx.grid.js Interim base class fixes, updates, additions --- .../assets/modext/widgets/core/modx.grid.js | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index 348a268e69..c1d5a01914 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -446,12 +446,12 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { } else { handler = function(item) { const - { options } = item.options, + { options } = item, { id } = this.menu.record, doAction = (id, options) => { const action = Ext.urlEncode(options.params || { action: options.action }), - query = `?id=${id}&${action}`, + query = `?${action}&id=${id}`, content = Ext.get('modx_content') ; if (content === null) { @@ -567,7 +567,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { case 'modx-grid-role': { const isAuthorityField = e.field === 'authority', - roleIsAssigned = e.record.json.isAssigned + roleIsAssigned = e.record[this.permissionsProviderProp].isAssigned ; if (roleIsAssigned && isAuthorityField) { return false; @@ -610,24 +610,6 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return classes; }, - /** - * @property {Function} setEditableColumnAccess - Enable/disable column editor based on user permissions - * - * @param {Array} columnIds - The ids of the columns that have an editor configured in the column model - * - * @return void - */ - setEditableColumnAccess: function(columnIds) { - if (!this.userCanEdit && !Ext.isEmpty(columnIds)) { - const colModel = this.getColumnModel(); - columnIds = columnIds.map(item => item.trim()); - columnIds.forEach(colId => { - const colIndex = colModel.getIndexById(colId); - colModel.setEditable(colIndex, false); - }); - } - }, - // -> User- /User Group-Level Permissions Checks for the calling "class" object /** @@ -714,6 +696,10 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return !Ext.isEmpty(objPermissions) && objPermissions.update === true; }, + userCanDeleteRecords: function(records) { + return records.some(record => this.userCanDeleteRecord(record)); + }, + userCanDeleteRecord: function(record) { const objPermissions = record[this.permissionsProviderProp].permissions; return !Ext.isEmpty(objPermissions) && !record[this.permissionsProviderProp].isProtected && objPermissions.delete === true; @@ -1168,7 +1154,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { if (hasBulkActions && !canDeleteRecord && !markActiveRows) { rowClasses.push('disable-selection'); } - if (record.json.isProtected) { + if (record[this.grid.permissionsProviderProp].isProtected) { rowClasses.push('modx-protected-row'); } return rowClasses.length ? rowClasses.join(' ') : '' ; @@ -1190,18 +1176,26 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { handler = typeof createHandler === 'string' ? this[createHandler] : createHandler, - text = _(`${objectType.toLowerCase()}_create`) || _('create'), - hasPermission = typeof createPermission === 'boolean' - ? createPermission - : this[createPermission] + text = _(`${objectType.toLowerCase()}_create`) || _('create') ; return { text: text, cls: 'primary-button', handler: handler, + scope: this, listeners: { + /* + Note that because this method is typically called from a grid's + tbar config object, the permissions properties are not yet available; + thus it is important that the Create permission be assessed within the + button's render (or later) event, as is done below. + */ render: { fn: function(btn) { + const hasPermission = typeof createPermission === 'boolean' + ? createPermission + : this[createPermission] + ; if (!hasPermission) { btn.hide(); } @@ -1470,8 +1464,8 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return []; } selections.forEach(record => { - const deletableRecord = record.json.permissions.delete; - if (!record.json.isProtected && deletableRecord) { + const deletableRecord = record[this.permissionsProviderProp].permissions.delete; + if (!record[this.permissionsProviderProp].isProtected && deletableRecord) { const item = itemIdType === 'string' ? record.data[pk] : parseInt(record.data[pk], 10); removableItems.push(item); } From 8b4aa4fa8517a1f6279f6439e8843613f6519597 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 2 Dec 2024 13:58:12 -0500 Subject: [PATCH 39/39] User Recent Resources updates Formatting, code style changes only. Also moved menu config from php processor to js class method for consistency. --- .../User/GetRecentlyEditedResources.php | 34 +--- .../modx.grid.user.recent.resource.js | 164 +++++++++++------- 2 files changed, 109 insertions(+), 89 deletions(-) diff --git a/core/src/Revolution/Processors/Security/User/GetRecentlyEditedResources.php b/core/src/Revolution/Processors/Security/User/GetRecentlyEditedResources.php index d1dd77078d..7fa506c7d0 100644 --- a/core/src/Revolution/Processors/Security/User/GetRecentlyEditedResources.php +++ b/core/src/Revolution/Processors/Security/User/GetRecentlyEditedResources.php @@ -1,4 +1,5 @@ getOne('User')) { - $row = array_merge($row, + /** @disregard P1013 Intelephense can not find this User instance method (getPhoto), but it does exist and is available here */ + $row = array_merge( + $row, $user->get(['username']), $user->Profile->get(['fullname', 'email']), ['photo' => $user->getPhoto(64, 64)] @@ -124,33 +125,6 @@ public function prepareRow(xPDOObject $object) /** @var modUserGroup $group */ $row['group'] = ($group = $user->getOne('PrimaryGroup')) ? $group->get('name') : ''; } - - $row['menu'] = []; - $row['menu'][] = [ - 'text' => $this->modx->lexicon('resource_overview'), - 'params' => [ - 'a' => 'resource/data', - 'id' => $resource->get('id'), - 'type' => 'view', - ], - ]; - if ($this->modx->hasPermission('edit_document')) { - $row['menu'][] = [ - 'text' => $this->modx->lexicon('resource_edit'), - 'params' => [ - 'a' => 'resource/update', - 'id' => $resource->get('id'), - 'type' => 'edit', - ], - ]; - } - - $row['menu'][] = '-'; - $row['menu'][] = [ - 'text' => $this->modx->lexicon('resource_view'), - 'handler' => 'this.preview', - ]; - $row['link'] = $this->modx->makeUrl($resource->get('id'), $resource->get('context_key')); return $row; diff --git a/manager/assets/modext/widgets/security/modx.grid.user.recent.resource.js b/manager/assets/modext/widgets/security/modx.grid.user.recent.resource.js index cb749b5365..889da8326e 100644 --- a/manager/assets/modext/widgets/security/modx.grid.user.recent.resource.js +++ b/manager/assets/modext/widgets/security/modx.grid.user.recent.resource.js @@ -6,73 +6,119 @@ * @param {Object} config An object of options. * @xtype modx-grid-user-recent-resource */ -MODx.grid.RecentlyEditedResourcesByUser = function(config) { - config = config || {}; - var dateFormat = MODx.config.manager_date_format + ' ' + MODx.config.manager_time_format; - Ext.applyIf(config,{ - title: _('recent_docs') - ,url: MODx.config.connector_url - ,baseParams: { - action: 'Security/User/GetRecentlyEditedResources' - ,user: config.user - } - ,autosave: true - ,save_action: 'Resource/UpdateFromGrid' - ,pageSize: 10 - ,fields: ['id','pagetitle','description','editedon','deleted','published','context_key','menu', 'link', 'occurred'] - ,columns: [{ - header: _('id') - ,dataIndex: 'id' - ,width: 75 - ,fixed: true - },{ - header: _('pagetitle') - ,dataIndex: 'pagetitle' - ,renderer: { fn: function(v,md,record) { - return this.renderLink(v, { - href: '?a=resource/update&id=' + record.data.id - ,target: '_blank' - }); - }, scope: this } - },{ - header: _('editedon') - ,dataIndex: 'occurred' - ,renderer : Ext.util.Format.dateRenderer(dateFormat) - },{ - header: _('published') - ,dataIndex: 'published' - ,width: 120 - ,fixed: true - ,editor: { xtype: 'combo-boolean' ,renderer: 'boolean' } - }] - ,paging: true - ,listeners: { - afteredit: this.refresh - ,afterrender: this.onAfterRender - ,scope: this +MODx.grid.RecentlyEditedResourcesByUser = function(config = {}) { + const dateFormat = `${MODx.config.manager_date_format} ${MODx.config.manager_time_format}`; + Ext.applyIf(config, { + title: _('recent_docs'), + url: MODx.config.connector_url, + baseParams: { + action: 'Security/User/GetRecentlyEditedResources', + user: config.user + }, + autosave: true, + save_action: 'Resource/UpdateFromGrid', + pageSize: 10, + fields: [ + 'id', + 'pagetitle', + 'description', + 'editedon', + 'deleted', + 'published', + 'context_key', + 'menu', + 'link', + 'occurred' + ], + columns: [{ + header: _('id'), + dataIndex: 'id', + width: 75, + fixed: true + }, { + header: _('pagetitle'), + dataIndex: 'pagetitle', + renderer: { + fn: function(value, metaData, record) { + return this.renderLink(value, { + href: `?a=resource/update&id=${record.data.id}`, + target: '_blank' + }); + }, + scope: this + } + }, { + header: _('editedon'), + dataIndex: 'occurred', + renderer: Ext.util.Format.dateRenderer(dateFormat) + }, { + header: _('published'), + dataIndex: 'published', + width: 120, + fixed: true, + editor: { + xtype: 'combo-boolean', + renderer: 'boolean' + } + }], + paging: true, + listeners: { + afteredit: this.refresh, + afterrender: this.onAfterRender, + scope: this } }); - MODx.grid.RecentlyEditedResourcesByUser.superclass.constructor.call(this,config); + MODx.grid.RecentlyEditedResourcesByUser.superclass.constructor.call(this, config); }; -Ext.extend(MODx.grid.RecentlyEditedResourcesByUser,MODx.grid.Grid,{ +Ext.extend(MODx.grid.RecentlyEditedResourcesByUser, MODx.grid.Grid, { + getMenu: function() { + const + record = this.getSelectionModel().getSelected(), + menu = [] + ; + menu.push({ + text: _('resource_overview'), + params: { + a: 'resource/data', + type: 'view' + } + }); + if (MODx.perm.edit_document) { + menu.push({ + text: _('resource_edit'), + params: { + a: 'resource/update', + type: 'edit' + } + }); + } + menu.push('-'); + menu.push({ + text: _('resource_view'), + handler: this.preview + }); + + return menu; + }, + preview: function() { window.open(this.menu.record.link); - } - ,refresh: function() { - var tree = Ext.getCmp('modx-resource-tree'); + }, + refresh: function() { + const tree = Ext.getCmp('modx-resource-tree'); if (tree && tree.rendered) { tree.refresh(); } - } + }, // Workaround to resize the grid when in a dashboard widget - ,onAfterRender: function() { - var cnt = Ext.getCmp('modx-content') - // Dashboard widget "parent" (renderTo) - ,parent = Ext.get('modx-grid-user-recent-resource'); - - if (cnt && parent) { - cnt.on('afterlayout', function(elem, layout) { - var width = parent.getWidth(); + onAfterRender: function() { + const + contentCmp = Ext.getCmp('modx-content'), + grid = Ext.get('modx-grid-user-recent-resource') + ; + if (contentCmp && grid) { + contentCmp.on('afterlayout', function(elem, layout) { + const width = grid.getWidth(); // Only resize when more than 500px (else let's use/enable the horizontal scrolling) if (width > 500) { this.setWidth(width); @@ -81,4 +127,4 @@ Ext.extend(MODx.grid.RecentlyEditedResourcesByUser,MODx.grid.Grid,{ } } }); -Ext.reg('modx-grid-user-recent-resource',MODx.grid.RecentlyEditedResourcesByUser); +Ext.reg('modx-grid-user-recent-resource', MODx.grid.RecentlyEditedResourcesByUser);