diff --git a/_build/resolvers/resolver_02_tables.php b/_build/resolvers/resolver_02_tables.php index 7ed7039..d5235d5 100644 --- a/_build/resolvers/resolver_02_tables.php +++ b/_build/resolvers/resolver_02_tables.php @@ -56,7 +56,7 @@ } } foreach ($tableFields as $field) { - $manager->removeField($class, $field); + //$manager->removeField($class, $field); } // 2. Operate with indexes $indexes = []; diff --git a/assets/components/minishop3/css/mgr/main.css b/assets/components/minishop3/css/mgr/main.css index 9acaf30..c91222d 100644 --- a/assets/components/minishop3/css/mgr/main.css +++ b/assets/components/minishop3/css/mgr/main.css @@ -89,11 +89,12 @@ a.x-menu-item .x-menu-item-text .icon { line-height: 16px; top: auto; } .ms3-btn-action {padding: 3px 20px 3px 6px!important;} .ms3-btn-action i.icon {padding-right: 3px;width: 15px;} /* Windows */ -.ms3-window .x-form-label-top .x-form-item label.x-form-item-label { padding-top: 15px !important; } +/*.ms3-window .x-form-label-top .x-form-item label.x-form-item-label { padding-top: 15px !important; }*/ .ms3-window .x-panel-mc, .ms3-window .x-tab-panel-bwrap > .x-tab-panel-body > .x-panel { margin-top: -14px; } .ms3-window .x-window-body.tabs { padding: 5px 10px 0 10px; } .ms3-window .desc { font-style: italic; padding-top: 5px; color: #555555; } +.ms3-window .text-success { color: green; } /* Tree */ .ms3-panel .x-tree-root-ct, .ms3-window .x-tree-root-ct { overflow: visible; } diff --git a/assets/components/minishop3/js/mgr/utilities/extrafield/grid.js b/assets/components/minishop3/js/mgr/utilities/extrafield/grid.js new file mode 100644 index 0000000..005abb1 --- /dev/null +++ b/assets/components/minishop3/js/mgr/utilities/extrafield/grid.js @@ -0,0 +1,204 @@ +ms3.grid.ExtraField = function (config) { + config = config || {}; + if (!config.id) { + config.id = 'ms3-grid-extrafields'; + } + config.disableContextMenuAction = true; + config.class_key = null; + + Ext.applyIf(config, { + baseParams: { + action: 'MiniShop3\\Processors\\Utilities\\ExtraField\\GetList' + }, + stateful: true, + stateId: config.id, + multi_select: true, + }); + ms3.grid.ExtraField.superclass.constructor.call(this, config); +}; + +Ext.extend(ms3.grid.ExtraField, ms3.grid.Default, { + + getFields: function () { + return ['id', 'class', 'key', 'label', 'dbtype', 'exists', 'active', 'actions']; + }, + + getTopBar: function () { + return [{ + text: ' ' + _('ms3_btn_create'), + handler: this.createExtraField, + scope: this + }, '->', this.getSearchField()]; + }, + + getColumns: function () { + return [ + { + header: _('ms3_id'), + dataIndex: 'id', + width: 50, + sortable: true + }, { + header: _('ms3_extrafields_class'), + dataIndex: 'class', + width: 150, + sortable: true, + hidden: true + }, { + header: _('ms3_extrafields_key'), + dataIndex: 'key', + width: 100, + sortable: true + }, { + header: _('ms3_extrafields_label'), + dataIndex: 'label', + width: 100, + sortable: true + }, { + header: _('ms3_extrafields_dbtype'), + dataIndex: 'dbtype', + width: 100, + sortable: true + }, { + header: _('ms3_extrafields_exists'), + dataIndex: 'exists', + width: 80, + sortable: true, + renderer: ms3.utils.renderBoolean + }, { + header: _('ms3_active'), + dataIndex: 'active', + width: 80, + sortable: true, + renderer: ms3.utils.renderBoolean + }, { + header: _('ms3_actions'), + dataIndex: 'actions', + id: 'actions', + width: 70, + renderer: ms3.utils.renderActions + } + ]; + }, + + getListeners: function () { + return { + rowDblClick: function (grid, rowIndex, e) { + const row = grid.store.getAt(rowIndex); + this.updateExtraField(grid, e, row); + }, + }; + }, + + extraFieldAction: function (method) { + const ids = this._getSelectedIds(); + if (!ids.length) { + return false; + } + MODx.Ajax.request({ + url: ms3.config.connector_url, + params: { + action: 'MiniShop3\\Processors\\Utilities\\ExtraField\\Multiple', + method: method, + ids: Ext.util.JSON.encode(ids), + }, + listeners: { + success: { + fn: function () { + //noinspection JSUnresolvedFunction + this.refresh(); + }, scope: this + }, + failure: { + fn: function (response) { + MODx.msg.alert(_('error'), response.message); + }, scope: this + }, + } + }) + }, + + createExtraField: function (btn, e) { + let w = Ext.getCmp('ms3-window-extra-field-create'); + if (w) { + w.close(); + } + w = MODx.load({ + xtype: 'ms3-window-extra-field-create', + id: 'ms3-window-extra-field-create', + listeners: { + success: { + fn: function () { + this.refresh(); + }, scope: this + } + } + }); + w.setValues({ + attributes: '', + default: '', + class: this.config.class_key + }); + w.show(e.target); + }, + + updateExtraField: function (btn, e, row) { + if (typeof(row) != 'undefined') { + this.menu.record = row.data; + } + const id = this.menu.record.id; + + MODx.Ajax.request({ + url: this.config.url, + params: { + action: 'MiniShop3\\Processors\\Utilities\\ExtraField\\Get', + id: id + }, + listeners: { + success: { + fn: function (r) { + let w = Ext.getCmp('ms3-window-extra-field-update'); + if (w) { + w.close(); + } + + w = MODx.load({ + xtype: 'ms3-window-extra-field-update', + id: 'ms3-window-extra-field-update', + record: r.object, + listeners: { + success: { + fn: function () { + this.refresh(); + }, scope: this + } + } + }); + w.fp.getForm().reset(); + w.fp.getForm().setValues(r.object); + w.show(e.target); + }, scope: this + } + } + }); + }, + + removeExtraField: function () { + const ids = this._getSelectedIds(); + + Ext.MessageBox.confirm( + _('ms3_menu_remove_title'), + ids.length > 1 + ? _('ms3_menu_remove_multiple_confirm') + : _('ms3_menu_remove_confirm'), + function (val) { + if (val == 'yes') { + this.extraFieldAction('Remove'); + } + }, + this + ); + }, + +}); +Ext.reg('ms3-grid-extrafields', ms3.grid.ExtraField); diff --git a/assets/components/minishop3/js/mgr/utilities/extrafield/tree.classes.js b/assets/components/minishop3/js/mgr/utilities/extrafield/tree.classes.js new file mode 100644 index 0000000..5fa2caf --- /dev/null +++ b/assets/components/minishop3/js/mgr/utilities/extrafield/tree.classes.js @@ -0,0 +1,84 @@ +/** + * Generates the Extra Fiels Classes Tree + * + * @class ms3.tree.ExtraFieldClass + * @extends MODx.tree.Tree + * @param {Object} config An object of options. + * @xtype ms3-tree-extrafieldclass + */ +MODx.tree.ExtraFieldClass = function (config) { + config = config || {}; + Ext.applyIf(config, { + title: _('user_groups'), + id: 'ms3-tree-extrafieldclass', + url: MODx.config.connector_url, + action: 'MiniShop3\\Processors\\Utilities\\ExtraField\\GetClassNodes', + rootIconCls: 'icon-group', + //root_id: 'n_ug_0', + //root_name: _('user_groups'), + enableDD: false, + rootVisible: false, + //ddAppendOnly: true, + useDefaultToolbar: false, + tbar: [], + + resize: { + fn: function(cmp) { + if (Ext.getCmp('ms3-grid-extrafields').hidden) { + cmp.layout.west.getSplitBar().el.hide(); + } + }, + scope: this + }, + refresh: { + fn: function() { + this.setActiveClassKey(); + }, + scope: this + } + }); + MODx.tree.ExtraFieldClass.superclass.constructor.call(this, config); +}; + +Ext.extend(MODx.tree.ExtraFieldClass, MODx.tree.Tree, { + windows: { + + }, + + /** + * Handles tree clicks + * @param {Object} n The node clicked + * @param {Object} e The event object + */ + _handleClick: function (n, e) { + e.stopEvent(); + e.preventDefault(); + + this.setActiveClassKey(n.attributes.type); + + if (this.disableHref) { return true; } + if (e.ctrlKey) { return true; } + return true; + }, + getMenu: function () { + return []; + }, + setActiveClassKey: function(class_key) { + const grid = Ext.getCmp('ms3-grid-extrafields'), + westPanel = Ext.getCmp('ms3-tree-panel-extrafield').layout.west + ; + if (class_key) { + grid.store.removeAll(); + grid.show(); + westPanel.getSplitBar().el.show(); + grid.config.class_key = class_key; + grid.store.baseParams.class = class_key; + grid.store.load(); + } else { + grid.hide(); + westPanel.getSplitBar().el.hide(); + + } + } +}); +Ext.reg('ms3-tree-extrafieldclass', MODx.tree.ExtraFieldClass); \ No newline at end of file diff --git a/assets/components/minishop3/js/mgr/utilities/extrafield/window.js b/assets/components/minishop3/js/mgr/utilities/extrafield/window.js new file mode 100644 index 0000000..f0ff2f2 --- /dev/null +++ b/assets/components/minishop3/js/mgr/utilities/extrafield/window.js @@ -0,0 +1,398 @@ +ms3.window.CreateExtraField = function (config) { + config = config || {}; + + Ext.applyIf(config, { + title: _('ms3_menu_create'), + width: 600, + baseParams: { + action: 'MiniShop3\\Processors\\Utilities\\ExtraField\\Create', + }, + }); + ms3.window.CreateExtraField.superclass.constructor.call(this, config); +}; + +Ext.extend(ms3.window.CreateExtraField, ms3.window.Default, { + + getFields: function (config) { + const existsInDatabase = (config.record !== undefined) ? config.record.exists : false; + const existsMessage = (config.record !== undefined) ? config.record.exists_message : ''; + return [ + { + xtype: 'hidden', + name: 'id', + id: config.id + '-id' + }, { + layout: 'column', + items: [{ + columnWidth: 1, + layout: 'form', + defaults: { msgTarget: 'under' }, + items: [{ + xtype: 'ms3-combo-combobox-default', + fieldLabel: _('ms3_extrafields_class'), + name: 'class', + hiddenName: 'class', + anchor: '99%', + id: config.id + '-class', + allowBlank: true, + disabled: existsInDatabase, + mode: 'local', + displayField: 'class', + valueField: 'class', + store: new Ext.data.ArrayStore({ + id: 0, + fields: ['class'], + data: [ + ['MiniShop3\\Model\\msProductData'], + ['MiniShop3\\Model\\msVendor'] + ] + }), + }], + }] + }, { + layout: 'column', + items: [{ + columnWidth: .33, + layout: 'form', + defaults: { msgTarget: 'under' }, + items: [{ + xtype: 'textfield', + fieldLabel: _('ms3_extrafields_key'), + name: 'key', + anchor: '99%', + id: config.id + '-key', + allowBlank: true, + disabled: existsInDatabase, + }], + }, { + columnWidth: .41, + layout: 'form', + items: [{ + xtype: 'ms3-combo-combobox-default', + fieldLabel: _('ms3_extrafields_xtype'), + name: 'xtype', + hiddenName: 'xtype', + anchor: '99%', + id: config.id + '-xtype', + allowBlank: true, + editable: true, + forceSelection: false, + //disabled: existsInDatabase, + mode: 'local', + displayField: 'value', + valueField: 'value', + store: new Ext.data.ArrayStore({ + id: 0, + fields: ['value'], + data: [ + ['textfield'], + ['textarea'] + ] + }), + }], + }, { + columnWidth: .25, + layout: 'form', + items: [{ + xtype: 'xcheckbox', + fieldLabel: _('ms3_active'), + boxLabel: _('ms3_active'), + name: 'active', + anchor: '99%', + id: config.id + '-active', + style: { paddingTop: '10px' }, + disabled: !existsInDatabase, + }], + }] + }, { + layout: 'column', + items: [{ + columnWidth: .33, + layout: 'form', + defaults: { msgTarget: 'under' }, + items: [{ + xtype: 'textfield', + fieldLabel: _('ms3_extrafields_label'), + name: 'label', + anchor: '99%', + id: config.id + '-label', + allowBlank: true, + //disabled: existsInDatabase, + }], + }, { + columnWidth: .66, + layout: 'form', + items: [{ + xtype: 'textfield', + fieldLabel: _('ms3_extrafields_description'), + name: 'description', + anchor: '99%', + id: config.id + '-description', + allowBlank: true, + //disabled: existsInDatabase, + }], + }] + }, { + xtype: 'displayfield', + cls: 'text-success', + html: existsMessage, + id: config.id + '-exists-message', + hidden: !existsInDatabase + }, { + id: config.id + '-create', + name: 'create', + xtype: 'hidden', + value: false + }, { + xtype: 'fieldset', + title: existsInDatabase ? _('ms3_extrafields_created') : _('ms3_extrafields_create'), + layout: 'column', + defaults: { msgTarget: 'under', border: false }, + checkboxToggle: !existsInDatabase, + cls: existsInDatabase ? '' : 'x-fieldset-checkbox-toggle', + collapsed: !existsInDatabase, + listeners: { + expand: { + fn: function (p) { + Ext.getCmp(config.id + '-create').setValue(true); + Ext.getCmp(config.id + '-active').enable(); + }, scope: this + }, + collapse: { + fn: function (p) { + Ext.getCmp(config.id + '-create').setValue(false); + Ext.getCmp(config.id + '-active').disable(); + Ext.getCmp(config.id + '-active').setValue(false); + }, scope: this + } + }, + items: [{ + columnWidth: 1, + layout: 'form', + items: [{ + layout: 'column', + items: [{ + columnWidth: .33, + layout: 'form', + defaults: { msgTarget: 'under' }, + items: [{ + xtype: 'ms3-combo-combobox-default', + fieldLabel: _('ms3_extrafields_dbtype'), + name: 'dbtype', + hiddenName: 'dbtype', + anchor: '99%', + id: config.id + '-dbtype', + allowBlank: true, + disabled: existsInDatabase, + mode: 'local', + displayField: 'value', + valueField: 'value', + store: new Ext.data.ArrayStore({ + id: 0, + fields: ['value'], + // https://cheatography.com/beeftornado/cheat-sheets/mysql-5-7-data-types/ + data: [ + ['tinyint'], + ['smallint'], + ['mediumint'], + ['int'], + ['bigint'], + ['float'], + ['double'], + ['decimal'], + ['char'], + ['varchar'], + ['tinytext'], + ['text'], + ['mediumtext'], + ['longtext'], + ['year'], + ['date'], + ['time'], + ['datetime'], + ['timestamp'] + ] + }), + }], + }, { + columnWidth: .33, + layout: 'form', + items: [{ + xtype: 'textfield', + fieldLabel: _('ms3_extrafields_precision'), + name: 'precision', + anchor: '99%', + id: config.id + '-precision', + allowBlank: true, + disabled: existsInDatabase + }] + }, { + columnWidth: .33, + layout: 'form', + items: [{ + xtype: 'ms3-combo-combobox-default', + fieldLabel: _('ms3_extrafields_phptype'), + name: 'phptype', + hiddenName: 'phptype', + anchor: '99%', + id: config.id + '-phptype', + allowBlank: true, + mode: 'local', + displayField: 'value', + valueField: 'value', + store: new Ext.data.ArrayStore({ + id: 0, + fields: ['value'], + data: [ + ['string'], + ['boolean'], + ['integer'], + ['float'], + ['json'], + ['datetime'] + ] + }) + }], + }] + }, { + layout: 'column', + items: [{ + columnWidth: .33, + layout: 'form', + defaults: { msgTarget: 'under' }, + items: [{ + xtype: 'ms3-combo-combobox-default', + fieldLabel: _('ms3_extrafields_attributes'), + name: 'attributes', + hiddenName: 'attributes', + anchor: '99%', + id: config.id + '-attributes', + allowBlank: true, + disabled: existsInDatabase, + mode: 'local', + displayField: 'title', + valueField: 'value', + store: new Ext.data.ArrayStore({ + id: 0, + fields: ['value', 'title'], + data: [ + ['', _('no')], + ['BINARY', 'BINARY'], + ['UNSIGNED', 'UNSIGNED'], + ['UNSIGNED ZEROFILL', 'UNSIGNED ZEROFILL'], + ['on update CURRENT_TIMESTAMP', 'on update CURRENT_TIMESTAMP'], + ] + }) + }], + }, { + columnWidth: .33, + layout: 'form', + items: [{ + xtype: 'ms3-combo-combobox-default', + fieldLabel: _('ms3_extrafields_default'), + name: 'default', + hiddenName: 'default', + anchor: '99%', + id: config.id + '-default', + allowBlank: true, + disabled: existsInDatabase, + mode: 'local', + displayField: 'title', + valueField: 'value', + store: new Ext.data.ArrayStore({ + id: 0, + fields: ['value', 'title'], + data: [ + ['', _('no')], + ['NULL', 'NULL'], + ['CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP'], + ['USER_DEFINED', 'Как определено:'] + ] + }), + listeners: { + afterrender: { + fn: function (select, rec) { + this.handleDefaultFields(select); + }, scope: this + }, + select: { + fn: function (select, rec) { + this.handleDefaultFields(select); + }, scope: this + } + } + }] + }, { + columnWidth: .33, + layout: 'form', + items: [{ + xtype: 'textfield', + fieldLabel: _('ms3_extrafields_default_value'), + name: 'default_value', + anchor: '99%', + id: config.id + '-default_value', + allowBlank: true, + disabled: existsInDatabase, + }], + }] + }, { + xtype: 'xcheckbox', + //fieldLabel: _('ms3_extrafields_null'), + boxLabel: _('ms3_extrafields_null'), + name: 'null', + anchor: '99%', + id: config.id + '-null', + allowBlank: true, + disabled: existsInDatabase, + }] + }] + } + ]; + }, + + handleDefaultFields: function (select) { + const value = select.getValue(); + let defaultValueElement = Ext.getCmp(this.config.id + '-default_value'); + if (value === 'USER_DEFINED') { + defaultValueElement.show(); + } else { + defaultValueElement.setValue(''); + defaultValueElement.hide(); + } + }, + +}); +Ext.reg('ms3-window-extra-field-create', ms3.window.CreateExtraField); + + +ms3.window.UpdateExtraField = function (config) { + config = config || {}; + + Ext.applyIf(config, { + title: _('ms3_menu_update'), + baseParams: { + action: 'MiniShop3\\Processors\\Utilities\\ExtraField\\Update', + } + }); + ms3.window.UpdateExtraField.superclass.constructor.call(this, config); +}; +Ext.extend(ms3.window.UpdateExtraField, ms3.window.CreateExtraField, { + + getFields: function (config) { + const fields = ms3.window.CreateExtraField.prototype.getFields.call(this, config); + + for (const i in fields) { + if (!fields.hasOwnProperty(i)) { + continue; + } + //const field = fields[i]; + //if (field.name === 'type') { + // field.disabled = true; + //} + } + + return fields; + } + +}); +Ext.reg('ms3-window-extra-field-update', ms3.window.UpdateExtraField); diff --git a/assets/components/minishop3/js/mgr/utilities/panel.js b/assets/components/minishop3/js/mgr/utilities/panel.js index 749adcc..45b648a 100644 --- a/assets/components/minishop3/js/mgr/utilities/panel.js +++ b/assets/components/minishop3/js/mgr/utilities/panel.js @@ -38,12 +38,71 @@ ms3.panel.Utilities = function (config) { xtype: 'ms3-utilities-import', cls: 'main-wrapper', }] + }, + { + title: _('ms3_extrafields'), + layout: 'form', + autoHeight: true, + items: [{ + html: _('ms3_extrafields_intro'), + bodyCssClass: 'panel-desc', + }, { + layout: 'border', + id: 'ms3-tree-panel-extrafield', + height: 500, + flex: 0, + border: false, + defaults: { + border: false, + bodyStyle: 'background-color:transparent;' + } + , items: [ + { + region: 'west', + cls: 'main-wrapper', + collapseMode: 'mini', + split: true, + useSplitTips: true, + monitorResize: true, + width: 280, + minWidth: 280, + minSize: 280, + maxSize: 400, + layout: 'fit', + items: [{ + xtype: 'ms3-tree-extrafieldclass' + }] + }, { + region: 'center', + id: 'ms3-grid-extrafields', + xtype: 'ms3-grid-extrafields', + hidden: true, + layout: 'fit', + cls: 'main-wrapper' + } + ] + }] } ] }] }); ms3.panel.Utilities.superclass.constructor.call(this, config); + + Ext.getCmp('ms3-grid-extrafields').store.on('load', this.fixExtraFieldsPanelHeight); + }; -Ext.extend(ms3.panel.Utilities, MODx.Panel); +Ext.extend(ms3.panel.Utilities, MODx.Panel, { + fixExtraFieldsPanelHeight: function () { + var gridExtraFields = Ext.getCmp('ms3-grid-extrafields'); + var extraFieldsPanel = Ext.getCmp('ms3-tree-panel-extrafield'); + + if (gridExtraFields.rendered && extraFieldsPanel.rendered) { + var gridHeight = gridExtraFields.getHeight(); + var maxHeight = (gridHeight > 500) ? gridHeight : 500; + extraFieldsPanel.setHeight(maxHeight); + Ext.getCmp('modx-content').doLayout(); + } + } +}); Ext.reg('ms3-utilities', ms3.panel.Utilities); diff --git a/core/components/minishop3/controllers/mgr/utilities.class.php b/core/components/minishop3/controllers/mgr/utilities.class.php index b19ee5a..b86dafa 100644 --- a/core/components/minishop3/controllers/mgr/utilities.class.php +++ b/core/components/minishop3/controllers/mgr/utilities.class.php @@ -31,12 +31,22 @@ public function getLanguageTopics() */ public function loadCustomCssJs() { + $this->addCss($this->ms3->config['cssUrl'] . 'mgr/bootstrap.buttons.css'); + $this->addCss($this->ms3->config['cssUrl'] . 'mgr/main.css'); $this->addCss($this->ms3->config['cssUrl'] . 'mgr/utilities/gallery.css'); $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/minishop3.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/misc/default.grid.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/misc/default.window.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/misc/ms3.utils.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/misc/ms3.combo.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/utilities/panel.js'); $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/utilities/gallery/panel.js'); $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/utilities/import/panel.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/utilities/extrafield/tree.classes.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/utilities/extrafield/grid.js'); + $this->addJavascript($this->ms3->config['jsUrl'] . 'mgr/utilities/extrafield/window.js'); $config = $this->ms3->config; diff --git a/core/components/minishop3/lexicon/ru/manager.inc.php b/core/components/minishop3/lexicon/ru/manager.inc.php index 5437aa9..042655f 100644 --- a/core/components/minishop3/lexicon/ru/manager.inc.php +++ b/core/components/minishop3/lexicon/ru/manager.inc.php @@ -234,3 +234,22 @@ $_lang['ms3_utilities_scheduler_nf'] = 'У вас не установлен компонент Scheduler'; $_lang['ms3_utilities_scheduler_task_ce'] = 'Не удалось создать задание Scheduler'; $_lang['ms3_utilities_scheduler_success'] = 'Задание Scheduler создано'; + +$_lang['ms3_extrafields'] = 'Расширение объектов'; +$_lang['ms3_extrafields_intro'] = 'Здесь вы можете создать дополнительные поля для объектов MiniShop3'; +$_lang['ms3_extrafields_class'] = 'Объект'; +$_lang['ms3_extrafields_key'] = 'Ключ поля'; +$_lang['ms3_extrafields_label'] = 'Заголовок'; +$_lang['ms3_extrafields_description'] = 'Описание'; +$_lang['ms3_extrafields_xtype'] = 'X-type'; +$_lang['ms3_extrafields_dbtype'] = 'Тип поля'; +$_lang['ms3_extrafields_precision'] = 'Точность'; +$_lang['ms3_extrafields_phptype'] = 'Тип поля (php)'; +$_lang['ms3_extrafields_attributes'] = 'Аттрибуты'; +$_lang['ms3_extrafields_default'] = 'По-умолчанию'; +$_lang['ms3_extrafields_default_value'] = 'Значение по-умолчанию'; +$_lang['ms3_extrafields_null'] = 'Может быть NULL'; +$_lang['ms3_extrafields_exists'] = 'Существует в БД'; +$_lang['ms3_extrafields_exists_msg'] = 'Столбец [[+column]] существует в таблице [[+table]], редактирование части полей недоступно.'; +$_lang['ms3_extrafields_create'] = 'Создать поле в БД'; +$_lang['ms3_extrafields_created'] = 'Конфигурация поля в БД'; \ No newline at end of file diff --git a/core/components/minishop3/schema/minishop3.mysql.schema.xml b/core/components/minishop3/schema/minishop3.mysql.schema.xml index fe7beb2..1a1ca13 100644 --- a/core/components/minishop3/schema/minishop3.mysql.schema.xml +++ b/core/components/minishop3/schema/minishop3.mysql.schema.xml @@ -535,4 +535,26 @@ owner="foreign"/> + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/components/minishop3/src/MiniShop3.php b/core/components/minishop3/src/MiniShop3.php index 129876b..173f487 100644 --- a/core/components/minishop3/src/MiniShop3.php +++ b/core/components/minishop3/src/MiniShop3.php @@ -10,6 +10,7 @@ use MiniShop3\Controllers\Order\OrderStatus; use MiniShop3\Controllers\Payment\PaymentInterface; use MiniShop3\Model\msOrder; +use MiniShop3\Utils\ExtraFields; use MiniShop3\Utils\Format; use MiniShop3\Utils\Plugins; use MiniShop3\Utils\Services; @@ -57,6 +58,9 @@ class MiniShop3 /** @var Plugins $plugins */ public $plugins; + /** @var ExtraFields $extraFields */ + public $extraFields; + /** @var Options $options */ public $options; @@ -107,6 +111,7 @@ public function __construct(modX $modx, array $config = []) $this->format = new Format($this); $this->services = new Services($this); //$this->plugins = new Plugins($this); + $this->extraFields = new ExtraFields($this->modx); $this->options = new Options($this); $this->deleteOldDraft(); @@ -296,6 +301,7 @@ public function initialize($ctx = 'web', $scriptProperties = []) */ public function loadMap() { + $this->extraFields->loadMap(); if (method_exists($this->pdoFetch, 'makePlaceholders')) { // $plugins = $this->plugins->load(); // foreach ($plugins as $plugin) { diff --git a/core/components/minishop3/src/Model/msExtraField.php b/core/components/minishop3/src/Model/msExtraField.php new file mode 100644 index 0000000..43a056d --- /dev/null +++ b/core/components/minishop3/src/Model/msExtraField.php @@ -0,0 +1,26 @@ + 'MiniShop3\\Model', + 'version' => '3.0', + 'table' => 'ms3_extra_fields', + 'extends' => 'xPDO\\Om\\xPDOSimpleObject', + 'tableMeta' => [ + 'engine' => 'InnoDB', + ], + 'fields' => [ + 'class' => null, + 'key' => null, + 'label' => null, + 'description' => null, + 'xtype' => null, + 'dbtype' => null, + 'precision' => null, + 'phptype' => null, + 'null' => 0, + 'default' => null, + 'attributes' => null, + 'active' => 0, + ], + 'fieldMeta' => [ + 'class' => [ + 'dbtype' => 'varchar', + 'precision' => '191', + 'phptype' => 'string', + 'null' => true, + ], + 'key' => [ + 'dbtype' => 'varchar', + 'precision' => '64', + 'phptype' => 'string', + 'null' => true, + ], + 'label' => [ + 'dbtype' => 'varchar', + 'precision' => '191', + 'phptype' => 'string', + 'null' => true, + ], + 'description' => [ + 'dbtype' => 'varchar', + 'precision' => '191', + 'phptype' => 'string', + 'null' => true, + ], + 'xtype' => [ + 'dbtype' => 'varchar', + 'precision' => '191', + 'phptype' => 'string', + 'null' => true, + ], + 'dbtype' => [ + 'dbtype' => 'varchar', + 'precision' => '100', + 'phptype' => 'string', + 'null' => true, + ], + 'precision' => [ + 'dbtype' => 'varchar', + 'precision' => '100', + 'phptype' => 'string', + 'null' => true, + ], + 'phptype' => [ + 'dbtype' => 'varchar', + 'precision' => '100', + 'phptype' => 'string', + 'null' => true, + ], + 'null' => [ + 'dbtype' => 'tinyint', + 'precision' => '1', + 'phptype' => 'boolean', + 'null' => true, + 'default' => 1, + ], + 'default' => [ + 'dbtype' => 'varchar', + 'precision' => '191', + 'phptype' => 'string', + 'null' => true, + ], + 'attributes' => [ + 'dbtype' => 'varchar', + 'precision' => '191', + 'phptype' => 'string', + 'null' => true, + ], + 'active' => [ + 'dbtype' => 'tinyint', + 'precision' => '1', + 'phptype' => 'boolean', + 'null' => true, + 'default' => 0, + ] + ], + 'indexes' => [], + 'composites' => [], + ]; +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/Create.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/Create.php new file mode 100644 index 0000000..4bec81d --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/Create.php @@ -0,0 +1,106 @@ +modx->hasPermission($this->permission)) { + return $this->modx->lexicon('access_denied'); + } + + $this->extraFields = new ExtraFields($this->modx); + + return parent::initialize(); + } + + + /** + * @return bool + */ + public function beforeSet() + { + $this->createColumn = filter_var($this->getProperty('create'), FILTER_VALIDATE_BOOLEAN); + + $required = ['class', 'key']; + if ($this->createColumn) { + $required = array_merge($required, ['dbtype', 'phptype']); + } + foreach ($required as $field) { + if (!$tmp = trim($this->getProperty($field))) { + $this->addFieldError($field, $this->modx->lexicon('field_required')); + } else { + $this->setProperty($field, $tmp); + } + } + + if ($this->hasErrors()) { + return false; + } + + $className = $this->getProperty('class'); + $key = $this->getProperty('key'); + $classFields = $this->modx->getFields($className); + if (!empty($classFields)) { + if (array_key_exists($key, $classFields)) { + $this->addFieldError('key', $this->modx->lexicon('ms3_err_ae')); + } + } + + $doesAlreasyExistCriteria = [ + 'class' => $this->getProperty('class'), + 'key' => $this->getProperty('key') + ]; + if ($this->doesAlreadyExist($doesAlreasyExistCriteria)) { + $this->modx->error->addField('key', $this->modx->lexicon('ms3_err_ae')); + } + + return !$this->hasErrors(); + } + + /** + * @return array + */ + public function beforeSave() + { + if ($this->createColumn) { + + $class = $this->object->get('class'); + $key = $this->object->get('key'); + // TODO: Не однозначное поведение, если в базе существует столбец, а пользователь создаст с другим dbtype + if (!$this->extraFields->columnExists($class, $key)) { + if (!$this->extraFields->addColumn($this->object)) { + // TODO: заменить текст ошибки на "Ошибка добавления поля" + $this->modx->error->addField('key', $this->modx->lexicon('ms3_err_unknown')); + } + } + } + + return parent::beforeSave(); + } + + public function afterSave() + { + $this->extraFields->clearCache(); + return parent::afterSave(); + } +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/Get.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/Get.php new file mode 100644 index 0000000..e5c4edf --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/Get.php @@ -0,0 +1,52 @@ +modx->hasPermission($this->permission)) { + return $this->modx->lexicon('access_denied'); + } + + return parent::initialize(); + } + + /** + * {@inheritDoc} + * @return mixed + */ + public function beforeOutput() + { + $extraFieldsManager = new ExtraFields($this->modx); + + $className = $this->object->get('class'); + $columnName = $this->object->get('key'); + $table = $this->modx->getTableName($className); + $exists = $extraFieldsManager->columnExists($className, $columnName); + + $this->object->set('exists', $exists); + $existsMessage = ''; + if ($exists) { + $existsMessage = $this->modx->lexicon('ms3_extrafields_exists_msg', ['column' => $columnName, 'table' =>$table]); + } + $this->object->set('exists_message', $existsMessage); + + parent::beforeOutput(); + } +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/GetClassNodes.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/GetClassNodes.php new file mode 100644 index 0000000..fa4660f --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/GetClassNodes.php @@ -0,0 +1,115 @@ +modx->hasPermission($this->permission); + } + + /** + * {@inheritDoc} + * @return array + */ + public function getLanguageTopics() + { + return $this->languageTopics; + } + + /** + * {@inheritDoc} + * @return mixed + */ + public function initialize() + { + $this->setDefaultProperties([ + 'id' => 0, + 'sort' => 'name', + 'dir' => 'ASC', + 'showAnonymous' => true, + ]); + + return true; + } + + /** + * {@inheritDoc} + * @return mixed + */ + public function process() + { + $classes = []; + if ($this->getProperty('id') == 'root') { + $classes = $this->getClasses(); + } + + $list = []; + /** @var modUserGroup $group */ + foreach ($classes as $class => $props) { + $list[] = $this->prepareClass($class, $props); + } + + return $this->toJSON($list); + } + + /** + * Get the User Groups within the filter + * @return array + */ + public function getClasses() + { + return [ + msProductData::class => [ + //'text' => $this->modx->lexicon('ms3_product') . ' (msProductData)', + 'iconCls' => $this->modx->getOption('mgr_tree_icon_msproduct', null, 'icon icon-tag') + ], + msVendor::class => [ + 'iconCls' => 'icon icon-industry' + ], + //msOrder::class => [ + //], + ]; + } + + /** + * Prepare a User Group for listing + * @param modUserGroup $group + * @return array + */ + public function prepareClass(string $classKey, array $props) + { + $cls = ''; + + $path = explode('\\', $classKey); + $shortClass = array_pop($path); + + $itemArray = array_merge([ + 'text' => htmlentities($shortClass), + 'id' => 'n_' . str_replace('\\', '__', $classKey), + 'hasChildren' => false, + 'expanded' => false, + 'childCount' => 0, + 'type' => $classKey, + 'qtip' => $classKey, + 'cls' => $cls, + 'allowDrop' => false, + 'iconCls' => 'icon icon-dice-d6', + ], $props); + + return $itemArray; + } +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/GetList.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/GetList.php new file mode 100644 index 0000000..5f7574d --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/GetList.php @@ -0,0 +1,107 @@ +extraFieldsManager = new ExtraFields($this->modx); + + return parent::initialize(); + } + + /** + * @param xPDOQuery $c + * + * @return xPDOQuery + */ + public function prepareQueryBeforeCount(xPDOQuery $c) + { + if ($this->getProperty('combo')) { + $c->select('id,key'); + } + if ($id = (int)$this->getProperty('id')) { + $c->where(['id' => $id]); + } + + if ($class = trim($this->getProperty('class'))) { + $c->where(['class' => $class]); + } + + if ($query = trim($this->getProperty('query'))) { + $c->where([ + 'key:LIKE' => "%{$query}%", + 'OR:label:LIKE' => "%{$query}%", + ]); + } + + return $c; + } + + + /** + * @param xPDOObject $object + * + * @return array + */ + public function prepareRow(xPDOObject $object) + { + if ($this->getProperty('combo')) { + $data = [ + 'id' => $object->get('id'), + 'key' => $object->get('key'), + ]; + } else { + $data = $object->toArray(); + $data['exists'] = $this->extraFieldsManager->columnExists($data['class'], $data['key']); + $data['dbtype'] = empty($data['precision']) + ? $data['dbtype'] + : sprintf("%s (%s)", $data['dbtype'], $data['precision']); + + $data['actions'] = []; + + $data['actions'][] = [ + 'cls' => '', + 'icon' => 'icon icon-edit', + 'title' => $this->modx->lexicon('ms3_menu_update'), + 'action' => 'updateExtraField', + 'button' => true, + 'menu' => true, + ]; + + $data['actions'][] = [ + 'cls' => [ + 'menu' => 'red', + 'button' => 'red', + ], + 'icon' => 'icon icon-trash-o', + 'title' => $this->modx->lexicon('ms3_menu_remove'), + 'multiple' => $this->modx->lexicon('ms3_menu_remove_multiple'), + 'action' => 'removeExtraField', + 'button' => true, + 'menu' => true, + ]; + } + + return $data; + } +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/Multiple.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/Multiple.php new file mode 100644 index 0000000..5c299a4 --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/Multiple.php @@ -0,0 +1,34 @@ +getProperty('method', false); + if (!$method) { + return $this->failure(); + } + $method = ucfirst($method); + $ids = json_decode($this->getProperty('ids'), true); + if (empty($ids)) { + return $this->success(); + } + + foreach ($ids as $id) { + $this->modx->runProcessor('MiniShop3\\Processors\\Utilities\\ExtraField\\' . $method, [ + 'id' => $id, + ]); + } + + return $this->success(); + } +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/Remove.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/Remove.php new file mode 100644 index 0000000..655aa9c --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/Remove.php @@ -0,0 +1,35 @@ +modx->hasPermission($this->permission)) { + return $this->modx->lexicon('access_denied'); + } + + return parent::initialize(); + } + + public function afterRemove() + { + $extraFields = new ExtraFields($this->modx); + $extraFields->clearCache(); + + return parent::afterRemove(); + } +} diff --git a/core/components/minishop3/src/Processors/Utilities/ExtraField/Update.php b/core/components/minishop3/src/Processors/Utilities/ExtraField/Update.php new file mode 100644 index 0000000..20fd696 --- /dev/null +++ b/core/components/minishop3/src/Processors/Utilities/ExtraField/Update.php @@ -0,0 +1,145 @@ +modx->hasPermission($this->permission)) { + return $this->modx->lexicon('access_denied'); + } + + $this->extraFields = new ExtraFields($this->modx); + + return parent::initialize(); + } + + /** + * @return bool + */ + public function beforeSet() + { + $existsInDb = $this->extraFields->columnExists($this->object->get('class'), $this->object->get('key')); + if ($existsInDb) { + $this->createColumn = false; + $required = []; + // We cannot change these fields if the field exists in the database + $this->unsetProperty('class'); + $this->unsetProperty('key'); + $this->unsetProperty('dbtype'); + $this->unsetProperty('precision'); + $this->unsetProperty('null'); + $this->unsetProperty('default'); + $this->unsetProperty('default_value'); + $this->unsetProperty('attributes'); + } else { + $this->createColumn = filter_var($this->getProperty('create'), FILTER_VALIDATE_BOOLEAN); + $required = ['class', 'key']; + if ($this->createColumn) { + $required = array_merge($required, ['dbtype', 'phptype']); + } + } + + foreach ($required as $field) { + if (!$tmp = trim($this->getProperty($field))) { + $this->addFieldError($field, $this->modx->lexicon('field_required')); + } else { + $this->setProperty($field, $tmp); + } + } + + if ($this->hasErrors()) { + return false; + } + + if (!$existsInDb) { + $class = $this->getProperty('class'); + $key = $this->getProperty('key'); + $doesAlreasyExistCriteria = [ + 'id:!=' => $this->object->get('id'), + 'class' => $class, + 'key' => $key + ]; + if ($this->doesAlreadyExist($doesAlreasyExistCriteria) || $this->doesBuiltIn($class, $key)) { + $this->modx->error->addField('key', $this->modx->lexicon('ms3_err_ae')); + } + } + + return !$this->hasErrors(); + } + + /** + * @return array + */ + public function beforeSave() + { + if ($this->createColumn) { + $class = $this->object->get('class'); + $key = $this->object->get('key'); + // TODO: Не однозначное поведение, если в базе существует столбец, а пользователь создаст с другим dbtype + if (!$this->extraFields->columnExists($class, $key)) { + if (!$this->extraFields->addColumn($this->object)) { + // TODO: заменить текст ошибки на "Ошибка добавления поля" + $this->modx->error->addField('key', $this->modx->lexicon('ms3_err_unknown')); + } + } + } + + return parent::beforeSave(); + } + + public function afterSave() + { + $this->extraFields->clearCache(); + return parent::afterSave(); + } + + + /** + * Checks if the specified field is built-in. + * + * @param string $key + * @return boolean + */ + private function doesBuiltIn(string $className, string $fieldName): bool + { + $builtInFields = [ + msProductData::class => [ + 'id', 'article', 'price', 'old_price', 'weight', 'image', 'thumb', 'vendor_id', + 'made_in', 'new', 'popular', 'favorite', 'tags', 'color', 'size', 'source_id' + ], + msVendor::class => [ + 'id', 'position', 'name', 'resource_id', 'country', 'logo', 'address', 'phone', 'email', + 'description', 'properties' + ] + ]; + if (!array_key_exists($className, $builtInFields)) { + $this->modx->log(xPDO::LOG_LEVEL_ERROR, 'The specified class is not supported: ' . $className); + return false; + } + $normalizedFieldName = strtolower(trim($fieldName)); + return in_array($normalizedFieldName, $builtInFields[$className]); + } +} diff --git a/core/components/minishop3/src/Utils/ExtraFields.php b/core/components/minishop3/src/Utils/ExtraFields.php new file mode 100644 index 0000000..e57b9bb --- /dev/null +++ b/core/components/minishop3/src/Utils/ExtraFields.php @@ -0,0 +1,168 @@ + 'ms3']; + + public function __construct(modX $modx) + { + $this->modx = $modx; + } + + /** + * Adds extra fields to the xPDO map + * + * @return void + */ + public function loadMap() + { + if (!$fieldsMeta = $this->modx->cacheManager->get($this->cacheKey, $this->cacheOptions)) { + $fieldsMeta = []; + $c = $this->modx->newQuery(msExtraField::class, ['active' => 1]); + $fields = $this->modx->getIterator(msExtraField::class, $c); + foreach ($fields as $field) { + $fieldsMeta[] = $this->getFieldInfo($field); + } + $this->modx->cacheManager->add($this->cacheKey, $fieldsMeta, 0, $this->cacheOptions); + } + + foreach ($fieldsMeta as $fieldMeta) { + $this->addFieldToMap($fieldMeta); + } + } + + /** + * Clears cache + * + * @return void + */ + public function clearCache() + { + // TODO: [apha] Проверить, что при общей очистке кеша MODx этот метод сработает + $this->modx->cacheManager->delete($this->cacheKey, $this->cacheOptions); + } + + /** + * Checks that a column exists in a table + * + * @param string $class + * @param string $columnName + * @return boolean + */ + public function columnExists(string $class, string $columnName): bool + { + if (!empty($class)) { + try { + $tableName = $this->modx->getTableName($class); + if ($tableName) { + $stmt = $this->modx->prepare("SHOW COLUMNS FROM {$tableName} LIKE ?"); + $stmt->execute([$columnName]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if ($result) { + return true; + } + } + } catch (PDOException $e) { + return false; + } + } + return false; + } + + /** + * Adds a column to a table + * + * @param msExtraField $msExtraField + * @return boolean + */ + public function addColumn(msExtraField $msExtraField): bool + { + $meta = $this->getFieldInfo($msExtraField); + if ($meta) { + $this->addFieldToMap($meta); + + $xpdoManager = $this->modx->getManager(); + return $xpdoManager->addField($msExtraField->get('class'), $msExtraField->get('key')); + } + + return false; + } + + /** + * Adds a field to the xPDO map + * + * @param array $meta + * @return void + */ + private function addFieldToMap(array $meta) + { + $class = $meta['class']; + $field = $meta['field']; + + $classMap = $this->modx->map[$class]; + $classMap['fields'][$field] = $meta['default']; + $classMap['fieldMeta'][$field] = $meta['meta']; + + $this->modx->map[$class] = $classMap; + } + + /** + * Prepares field info with metadata + * + * @param msExtraField $field + * @return null|array + */ + private function getFieldInfo(msExtraField $field): mixed + { + if ($field == null || !($field instanceof msExtraField)) { + return null; + } + + foreach (['class', 'key', 'dbtype', 'phptype'] as $required) { + if (empty($field->get($required))) { + return null; + } + } + + $meta = []; + foreach (['dbtype', 'phptype', 'null', 'precision', 'attributes'] as $k) { + $v = $field->get($k); + if (!empty($v)) { + $meta[$k] = $v; + } + } + + $default = $field->get('default'); + switch ($default) { + case 'NULL': + $meta['default'] = null; + break; + case 'CURRENT_TIMESTAMP': + $meta['default'] = 'CURRENT_TIMESTAMP'; + break; + case 'USER_DEFINED': + $meta['default'] = $field->get('default_value'); + break; + default: + break; + } + + return [ + 'class' => $field->get('class'), + 'field' => $field->get('key'), + 'default' => array_key_exists('default', $meta) ? $meta['default'] : null, + 'meta' => $meta + ]; + } +}