From abfe1552be4a4aa2c6f06897fc7d488a93fbe039 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Tue, 24 Feb 2026 09:56:56 +0600 Subject: [PATCH 1/3] feat(gallery): Vue refactor and unified modal styles - Replace legacy gallery JS (panel, toolbar, view, window) with Vue components - Add ProductGallery, ProductGalleryGrid, ProductGalleryToolbar, ProductGalleryEditDialog - Add useGalleryApi composable, integrate GalleryUploader - Unify modal width via --ms3-modal-width, fix ConfirmDialog width (90vw max) - Fix Edit Dialog close button: use closeButtonProps for circular focus ring - Add lexicon entries (ru/en), update Upload processor and product controller --- assets/components/minishop3/css/mgr/main.css | 67 --- .../js/mgr/product/gallery/gallery.panel.js | 142 ------ .../js/mgr/product/gallery/gallery.toolbar.js | 77 ---- .../js/mgr/product/gallery/gallery.view.js | 435 ------------------ .../js/mgr/product/gallery/gallery.window.js | 93 ---- .../controllers/product/update.class.php | 32 +- .../minishop3/lexicon/en/product.inc.php | 48 +- .../minishop3/lexicon/ru/product.inc.php | 48 +- .../src/Processors/Gallery/Upload.php | 5 + vueManager/src/components/ProductData.vue | 2 +- .../components/gallery/GalleryUploader.vue | 100 +++- .../src/components/product/ProductGallery.vue | 265 +++++++++++ .../product/ProductGalleryEditDialog.vue | 146 ++++++ .../components/product/ProductGalleryGrid.vue | 339 ++++++++++++++ .../product/ProductGalleryToolbar.vue | 162 +++++++ .../src/components/product/ProductTabs.vue | 56 +-- vueManager/src/composables/useGalleryApi.js | 239 ++++++++++ vueManager/src/entries/product-tabs.js | 2 + vueManager/src/scss/_variables.scss | 3 + vueManager/src/scss/primevue.scss | 6 + vueManager/vite.config.js | 2 + 21 files changed, 1397 insertions(+), 872 deletions(-) delete mode 100644 assets/components/minishop3/js/mgr/product/gallery/gallery.panel.js delete mode 100644 assets/components/minishop3/js/mgr/product/gallery/gallery.toolbar.js delete mode 100644 assets/components/minishop3/js/mgr/product/gallery/gallery.view.js delete mode 100644 assets/components/minishop3/js/mgr/product/gallery/gallery.window.js create mode 100644 vueManager/src/components/product/ProductGallery.vue create mode 100644 vueManager/src/components/product/ProductGalleryEditDialog.vue create mode 100644 vueManager/src/components/product/ProductGalleryGrid.vue create mode 100644 vueManager/src/components/product/ProductGalleryToolbar.vue create mode 100644 vueManager/src/composables/useGalleryApi.js diff --git a/assets/components/minishop3/css/mgr/main.css b/assets/components/minishop3/css/mgr/main.css index 8ce55bfa..868e0d9b 100644 --- a/assets/components/minishop3/css/mgr/main.css +++ b/assets/components/minishop3/css/mgr/main.css @@ -457,73 +457,6 @@ a.x-menu-item .x-menu-item-text .icon { height: auto; } -/* Gallery */ -#ms3-gallery-page.drag-over:after { - content: ""; - top: 0; - right: 0; - bottom: 0; - left: 0; - position: absolute; - display: block; - opacity: 0.1; - background: forestgreen; - border: 5px solid darkgreen; -} - -/* Images list */ -.ms3-gallery-images { - float: left; -} - -#ms3-gallery-images-view { - min-height: 150px; -} - -.ms3-gallery-thumb-wrap:hover .modx-browser-thumb { - border: 1px solid #aaa; -} - -.ms3-gallery-thumb { - width: 120px; - height: 90px; -} - -.ms3-gallery-thumb img { - max-width: 120px; - max-height: 90px; - display: block; - margin: auto; -} - -.ms3-gallery-window-thumb { - border: 1px solid #e4e4e4; - border-radius: 2px; - background: #fdfdfd; -} - -/* Gallery image window */ -.ms3-gallery-cba .x-form-cb-label { - margin-top: 6px; -} - -.ms3-gallery-window-link { - width: 100%; - display: block; - text-align: center; -} - -.ms3-gallery-window-details { - width: 100%; - font-size: 12px; -} - -.ms3-gallery-window-details th { - text-align: right; - padding-right: 5px; - width: 50%; -} - /* Settings */ .x-grid3-col-color { padding-top: 10px; diff --git a/assets/components/minishop3/js/mgr/product/gallery/gallery.panel.js b/assets/components/minishop3/js/mgr/product/gallery/gallery.panel.js deleted file mode 100644 index 7f5e0275..00000000 --- a/assets/components/minishop3/js/mgr/product/gallery/gallery.panel.js +++ /dev/null @@ -1,142 +0,0 @@ -ms3.panel.Gallery = function (config) { - config = config || {}; - - Ext.apply(config, { - border: false, - id: 'ms3-gallery-page', - baseCls: 'x-panel', - items: [{ - border: false, - style: {padding: '10px 5px'}, - xtype: 'ms3-gallery-page-toolbar', - id: 'ms3-gallery-page-toolbar', - record: config.record, - }, { - border: false, - style: {padding: '10px 5px'}, - html: '' - }, { - border: false, - style: {padding: '5px'}, - layout: 'anchor', - items: [{ - border: false, - xtype: 'ms3-gallery-images-panel', - id: 'ms3-gallery-images-panel', - cls: 'modx-pb-view-ct', - product_id: config.record.id, - pageSize: config.pageSize - }] - }] - }); - ms3.panel.Gallery.superclass.constructor.call(this, config); - - this.on('afterrender', function () { - const gallery = this; - window.setTimeout(function () { - gallery.initialize(); - }, 100); - }); -}; -Ext.extend(ms3.panel.Gallery, MODx.Panel, { - vueUploaderInstance: null, - - initialize: function () { - if (this.initialized) { - return; - } - this._initUploader(); - this.initialized = true; - }, - - _initUploader: function () { - if (typeof window.MS3_initGalleryUploader !== 'function') { - console.error('[MS3 Gallery] Vue uploader not loaded. Make sure gallery-uploader.min.js is included.'); - MODx.msg.alert(_('error'), 'Gallery uploader not loaded. Please refresh the page.'); - return; - } - - const allowedTypes = ms3.config.media_source.allowedFileTypes || MODx.config.upload_images || 'jpg,jpeg,png,gif,webp'; - const allowedFileTypes = allowedTypes.split(',').map(ext => { - const mimeTypes = { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif', - 'webp': 'image/webp', - 'avif': 'image/avif', - 'heic': 'image/heic' - }; - return mimeTypes[ext.trim()] || 'image/' + ext.trim(); - }); - - this.vueUploaderInstance = window.MS3_initGalleryUploader({ - containerId: 'ms3-gallery-uploader', - productId: this.record.id, - sourceId: this.record.source, - connectorUrl: ms3.config.connector_url, - maxFileSize: ms3.config.media_source.maxUploadSize || MODx.config.upload_maxsize || 10485760, - maxWidth: ms3.config.media_source.maxUploadWidth || 1920, - maxHeight: ms3.config.media_source.maxUploadHeight || 1080, - allowedFileTypes: allowedFileTypes, - onUploadSuccess: this.onUploadSuccess.bind(this), - onUploadError: this.onUploadError.bind(this), - onUploadComplete: this.onUploadComplete.bind(this) - }); - - if (!this.vueUploaderInstance) { - console.error('[MS3 Gallery] Failed to initialize Vue uploader'); - } - }, - - onUploadSuccess: function (data) { - const file = data.file; - const response = data.response; - console.log('[MS3 Gallery] Upload success:', file.name, response); - }, - - onUploadError: function (data) { - const file = data.file; - const error = data.error; - const fileName = file ? file.name : 'File'; - const errorMsg = error.message || 'Upload failed'; - console.error('[MS3 Gallery] Upload error:', fileName, error); - MODx.msg.alert(_('error'), fileName + ': ' + errorMsg); - }, - - onUploadComplete: function (result) { - console.log('[MS3 Gallery] Upload complete:', result); - - const panel = Ext.getCmp('ms3-gallery-images-panel'); - if (panel) { - panel.view.getStore().reload(); - - MODx.Ajax.request({ - url: ms3.config.connector_url, - params: { - action: 'MiniShop3\\Processors\\Product\\Get', - id: this.record.id - }, - listeners: { - success: { - fn: function (r) { - if (r.object && r.object.thumb) { - panel.view.updateThumb(r.object.thumb); - } - } - } - } - }); - } - }, - - destroy: function () { - if (this.vueUploaderInstance && this.vueUploaderInstance.destroy) { - this.vueUploaderInstance.destroy(); - this.vueUploaderInstance = null; - } - ms3.panel.Gallery.superclass.destroy.call(this); - } - -}); -Ext.reg('ms3-gallery-page', ms3.panel.Gallery); diff --git a/assets/components/minishop3/js/mgr/product/gallery/gallery.toolbar.js b/assets/components/minishop3/js/mgr/product/gallery/gallery.toolbar.js deleted file mode 100644 index 1aa8c127..00000000 --- a/assets/components/minishop3/js/mgr/product/gallery/gallery.toolbar.js +++ /dev/null @@ -1,77 +0,0 @@ -ms3.panel.Toolbar = function (config) { - config = config || {}; - - Ext.apply(config, { - id: 'ms3-gallery-page-toolbar', - items: [{ - text: ' ', - cls: 'ms3-btn-actions', - menu: [{ - text: ' ' + _('ms3_gallery_file_generate_all'), - cls: 'ms3-btn-action', - handler: function () { - this.fileAction('generateAllThumbs') - }, - scope: this, - }, '-', { - text: ' ' + _('ms3_gallery_file_delete_all'), - cls: 'ms3-btn-action', - handler: function () { - this.fileAction('deleteAllFiles') - }, - scope: this, - },] - },'->', { - xtype: 'displayfield', - html: '' + _('ms3_product_source') + ':  ' - }, '-', { - xtype: 'ms3-combo-source', - id: 'ms3-resource-source', - description: '[[+source_id]]
' + _('ms3_product_source_help'), - value: config.record.source_id || config.record.source, - name: 'source_id', - hiddenName: 'source_id', - listeners: { - select: { - fn: this.sourceWarning, - scope: this - } - } - }] - }); - ms3.panel.Toolbar.superclass.constructor.call(this, config); - this.config = config; -}; -Ext.extend(ms3.panel.Toolbar, Ext.Toolbar, { - - sourceWarning: function (combo) { - const source_id = this.config.record.source_id || this.config.record.source; - const sel_id = combo.getValue(); - if (source_id !== sel_id) { - Ext.Msg.confirm(_('warning'), _('ms3_product_change_source_confirm'), function (e) { - if (e === 'yes') { - combo.setValue(sel_id); - MODx.activePage.submitForm({ - success: { - fn: function (r) { - var page = 'resource/update'; - MODx.loadPage(page, 'id=' + r.result.object.id); - }, scope: this - } - }); - } else { - combo.setValue(source_id); - } - }, this); - } - }, - - fileAction: function (method) { - const view = Ext.getCmp('ms3-gallery-images-view'); - if (view && typeof view[method] === 'function') { - return view[method].call(view, arguments); - } - }, - -}); -Ext.reg('ms3-gallery-page-toolbar', ms3.panel.Toolbar); diff --git a/assets/components/minishop3/js/mgr/product/gallery/gallery.view.js b/assets/components/minishop3/js/mgr/product/gallery/gallery.view.js deleted file mode 100644 index 35d8bf3b..00000000 --- a/assets/components/minishop3/js/mgr/product/gallery/gallery.view.js +++ /dev/null @@ -1,435 +0,0 @@ -ms3.panel.Images = function (config) { - config = config || {}; - - this.view = MODx.load({ - xtype: 'ms3-gallery-images-view', - id: 'ms3-gallery-images-view', - cls: 'ms3-gallery-images', - containerScroll: true, - pageSize: parseInt(config.pageSize || MODx.config.default_per_page), - product_id: config.product_id, - emptyText: _('ms3_gallery_emptymsg'), - }); - - Ext.applyIf(config, { - id: 'ms3-gallery-images', - cls: 'browser-view', - border: false, - items: [this.view], - tbar: this.getTopBar(config), - bbar: this.getBottomBar(config), - }); - ms3.panel.Images.superclass.constructor.call(this, config); - - const dv = this.view; - dv.on('render', function () { - dv.dragZone = new ms3.DragZone(dv); - dv.dropZone = new ms3.DropZone(dv); - }); -}; -Ext.extend(ms3.panel.Images, MODx.Panel, { - - _doSearch: function (tf) { - this.view.getStore().baseParams.query = tf.getValue(); - this.getBottomToolbar().changePage(1); - }, - - _clearSearch: function () { - this.view.getStore().baseParams.query = ''; - this.getBottomToolbar().changePage(1); - }, - - getTopBar: function () { - return new Ext.Toolbar({ - items: ['->', { - xtype: 'ms3-field-search', - width: 300, - listeners: { - search: { - fn: function (field) { - //noinspection JSUnresolvedFunction - this._doSearch(field); - }, scope: this - }, - clear: { - fn: function (field) { - field.setValue(''); - //noinspection JSUnresolvedFunction - this._clearSearch(); - }, scope: this - }, - }, - }] - }) - }, - - getBottomBar: function (config) { - return new Ext.PagingToolbar({ - pageSize: parseInt(config.pageSize || MODx.config.default_per_page), - store: this.view.store, - displayInfo: true, - autoLoad: true, - items: ['-', - _('per_page') + ':', - { - xtype: 'textfield', - value: parseInt(config.pageSize || MODx.config.default_per_page), - width: 50, - listeners: { - change: { - fn: function (tf, nv) { - if (Ext.isEmpty(nv)) { - return; - } - nv = parseInt(nv); - //noinspection JSUnresolvedFunction - this.getBottomToolbar().pageSize = nv; - this.view.getStore().load({params: {start: 0, limit: nv}}); - }, scope: this - }, - render: { - fn: function (cmp) { - new Ext.KeyMap(cmp.getEl(), { - key: Ext.EventObject.ENTER, - fn: function () { - this.fireEvent('change', this.getValue()); - this.blur(); - return true; - }, - scope: cmp - }); - }, scope: this - } - } - } - ] - }); - }, - -}); -Ext.reg('ms3-gallery-images-panel', ms3.panel.Images); - - -ms3.view.Images = function (config) { - config = config || {}; - - this._initTemplates(); - - Ext.applyIf(config, { - url: ms3.config.connector_url, - fields: [ - 'id', 'product_id', 'name', 'description', 'url', 'createdon', 'createdby', 'file', - 'thumbnail', 'source', 'source_name', 'type', 'position', 'active', 'properties', 'class', - 'add', 'alt', 'actions' - ], - id: 'ms3-gallery-images-view', - baseParams: { - action: 'MiniShop3\\Processors\\Gallery\\GetList', - product_id: config.product_id, - parent: 0, - type: 'image', - limit: config.pageSize || MODx.config.default_per_page - }, - //loadingText: _('loading'), - enableDD: true, - multiSelect: true, - tpl: this.templates.thumb, - itemSelector: 'div.modx-browser-thumb-wrap', - listeners: {}, - prepareData: this.formatData.createDelegate(this) - }); - ms3.view.Images.superclass.constructor.call(this, config); - - this.addEvents('sort', 'select'); - this.on('sort', this.onSort, this); - this.on('dblclick', this.onDblClick, this); - - const widget = this; - this.getStore().on('beforeload', function () { - widget.getEl().mask(_('loading'), 'x-mask-loading'); - }); - this.getStore().on('load', function () { - widget.getEl().unmask(); - }); -}; -Ext.extend(ms3.view.Images, MODx.DataView, { - - templates: {}, - windows: {}, - - onSort: function (o) { - const el = this.getEl(); - console.log('onSort', el) - el.mask(_('loading'), 'x-mask-loading'); - MODx.Ajax.request({ - url: ms3.config.connector_url, - params: { - action: 'MiniShop3\\Processors\\Gallery\\Sort', - product_id: this.config.product_id, - source_id: o.source.id, - target_id: o.target.id - }, - listeners: { - success: { - fn: function (r) { - el.unmask(); - this.store.reload(); - //noinspection JSUnresolvedFunction - this.updateThumb(r.object['thumb']); - }, scope: this - } - } - }); - }, - - onDblClick: function (e) { - const node = this.getSelectedNodes()[0]; - if (!node) { - return; - } - - this.cm.activeNode = node; - this.updateFile(node, e); - }, - - updateFile: function (btn, e) { - const node = this.cm.activeNode; - const data = this.lookup[node.id]; - if (!data) { - return; - } - - const w = MODx.load({ - xtype: 'ms3-gallery-image', - record: data, - listeners: { - success: { - fn: function () { - this.store.reload() - }, scope: this - } - } - }); - w.setValues(data); - w.show(e.target); - }, - - showFile: function () { - const node = this.cm.activeNode; - const data = this.lookup[node.id]; - if (!data) { - return; - } - - window.open(data.url); - }, - - fileAction: function (method) { - const ids = this._getSelectedIds(); - if (!ids.length) { - return false; - } - this.getEl().mask(_('loading'), 'x-mask-loading'); - MODx.Ajax.request({ - url: ms3.config.connector_url, - params: { - action: 'MiniShop3\\Processors\\Gallery\\Multiple', - method: method, - ids: Ext.util.JSON.encode(ids), - }, - listeners: { - success: { - fn: function (r) { - if (method === 'Remove') { - //noinspection JSUnresolvedFunction - this.updateThumb(r.object['thumb']); - } - this.store.reload(); - }, scope: this - }, - failure: { - fn: function (response) { - MODx.msg.alert(_('error'), response.message); - }, scope: this - }, - } - }) - }, - - deleteFiles: function () { - const ids = this._getSelectedIds(); - const title = ids.length > 1 - ? 'ms3_gallery_file_delete_multiple' - : 'ms3_gallery_file_delete'; - const message = ids.length > 1 - ? 'ms3_gallery_file_delete_multiple_confirm' - : 'ms3_gallery_file_delete_confirm'; - Ext.MessageBox.confirm( - _(title), - _(message), - function (val) { - if (val == 'yes') { - this.fileAction('Remove'); - } - }, - this - ); - }, - - deleteAllFiles: function () { - const product_id = this.config.product_id || ''; - - Ext.MessageBox.confirm( - _('ms3_gallery_file_delete_multiple'), - _('ms3_gallery_file_delete_multiple_confirm'), - function (val) { - if (val == 'yes') { - this.getEl().mask(_('loading'), 'x-mask-loading'); - MODx.Ajax.request({ - url: ms3.config.connector_url, - params: { - action: 'MiniShop3\\Processors\\Gallery\\RemoveAll', - product_id: product_id, - }, - listeners: { - success: { - fn: function (r) { - //noinspection JSUnresolvedFunction - this.updateThumb(r.object['thumb']); - this.store.reload(); - }, scope: this - }, - failure: { - fn: function (response) { - MODx.msg.alert(_('error'), response.message); - }, scope: this - }, - } - }) - } - }, - this - ); - }, - - generateThumbs: function () { - this.fileAction('Generate'); - }, - - generateAllThumbs: function () { - const product_id = this.config.product_id || ''; - - Ext.MessageBox.confirm( - _('ms3_gallery_file_generate_thumbs'), - _('ms3_gallery_file_generate_thumbs_confirm'), - function (val) { - if (val == 'yes') { - this.getEl().mask(_('loading'), 'x-mask-loading'); - MODx.Ajax.request({ - url: ms3.config.connector_url, - params: { - action: 'MiniShop3\\Processors\\Gallery\\GenerateAll', - product_id: product_id, - }, - listeners: { - success: { - fn: function (r) { - //noinspection JSUnresolvedFunction - this.updateThumb(r.object['thumb']); - this.store.reload(); - }, scope: this - }, - failure: { - fn: function (response) { - MODx.msg.alert(_('error'), response.message); - }, scope: this - }, - } - }) - } - }, - this - ); - }, - - updateThumb: function (url) { - const thumb = Ext.get('ms3-product-image'); - if (thumb && url) { - thumb.set({'src': url}); - } - }, - - run: function (p) { - p = p || {}; - const v = {}; - Ext.apply(v, this.store.baseParams); - Ext.apply(v, p); - this.changePage(1); - this.store.baseParams = v; - this.store.load(); - }, - - formatData: function (data) { - data.shortName = Ext.util.Format.ellipsis(data.name, 20); - data.createdon = ms3.utils.formatDate(data.createdon); - data.size = (data.properties['width'] && data.properties['height']) - ? data.properties['width'] + 'x' + data.properties['height'] - : ''; - if (data.properties['size'] && data.size) { - data.size += ', '; - } - data.size += data.properties['size'] - ? ms3.utils.formatSize(data.properties['size']) - : ''; - this.lookup['ms3_-gallery-image-' + data.id] = data; - return data; - }, - - _initTemplates: function () { - this.templates.thumb = new Ext.XTemplate( - '\ - \ - ' - ); - this.templates.thumb.compile(); - }, - - _showContextMenu: function (v, i, n, e) { - e.preventDefault(); - const data = this.lookup[n.id]; - const m = this.cm; - m.removeAll(); - - const menu = ms3.utils.getMenu(data.actions, this, this._getSelectedIds()); - for (const item in menu) { - if (!menu.hasOwnProperty(item)) { - continue; - } - m.add(menu[item]); - } - - m.show(n, 'tl-c?'); - m.activeNode = n; - }, - - _getSelectedIds: function () { - var ids = []; - const selected = this.getSelectedRecords(); - - for (const i in selected) { - if (!selected.hasOwnProperty(i)) { - continue; - } - ids.push(selected[i]['id']); - } - - return ids; - }, - -}); -Ext.reg('ms3-gallery-images-view', ms3.view.Images); diff --git a/assets/components/minishop3/js/mgr/product/gallery/gallery.window.js b/assets/components/minishop3/js/mgr/product/gallery/gallery.window.js deleted file mode 100644 index 82e59f0a..00000000 --- a/assets/components/minishop3/js/mgr/product/gallery/gallery.window.js +++ /dev/null @@ -1,93 +0,0 @@ -ms3.window.Image = function (config) { - config = config || {}; - - Ext.applyIf(config, { - title: config.record['name'], - width: 700, - baseParams: { - action: 'MiniShop3\\Processors\\Gallery\\Update', - }, - resizable: false, - maximizable: false, - minimizable: false, - }); - ms3.window.Image.superclass.constructor.call(this, config); -}; -Ext.extend(ms3.window.Image, ms3.window.Default, { - - getFields: function (config) { - const src = config.record['type'] === 'image' - ? config.record['url'] - : config.record['thumbnail']; - const img = MODx.config['connectors_url'] + 'system/phpthumb.php?src=' - + src - + '&w=333&h=198&f=jpg&q=90&zc=0&far=1&HTTP_MODAUTH=' - + MODx.siteId + '&wctx=mgr&source=' - + config.record['source']; - const fields = { - ms3_gallery_file_source: config.record['source_name'], - ms3_gallery_file_size: config.record['size'], - ms3_gallery_file_createdon: config.record['createdon'], - }; - let details = ''; - for (const i in fields) { - if (!fields.hasOwnProperty(i)) { - continue; - } - if (fields[i]) { - details += '' + _(i) + ':' + fields[i] + ''; - } - } - - return [ - {xtype: 'hidden', name: 'id', id: this.ident + '-id'}, - { - layout: 'column', - border: false, - anchor: '100%', - items: [{ - columnWidth: .5, - layout: 'form', - defaults: {msgTarget: 'under'}, - border: false, - items: [{ - xtype: 'displayfield', - hideLabel: true, - html: '\ - \ - \ - \ - ' + details + '' - }] - }, { - columnWidth: .5, - layout: 'form', - defaults: {msgTarget: 'under'}, - border: false, - items: [{ - xtype: 'textfield', - fieldLabel: _('ms3_gallery_file_name'), - name: 'file', - id: this.ident + '-file', - anchor: '100%' - }, { - xtype: 'textfield', - fieldLabel: _('ms3_gallery_file_title'), - name: 'name', - id: this.ident + '-name', - anchor: '100%' - }, { - xtype: 'textarea', - fieldLabel: _('ms3_gallery_file_description'), - name: 'description', - id: this.ident + '-description', - anchor: '100%', - height: 100 - }] - }] - } - ]; - } - -}); -Ext.reg('ms3-gallery-image', ms3.window.Image); diff --git a/core/components/minishop3/controllers/product/update.class.php b/core/components/minishop3/controllers/product/update.class.php index 4120e771..43ea3fcd 100644 --- a/core/components/minishop3/controllers/product/update.class.php +++ b/core/components/minishop3/controllers/product/update.class.php @@ -64,19 +64,11 @@ public function loadCustomCssJs() // Product Tabs Vue module (contains Properties, Gallery, Categories, Links, Options tabs) $this->addCss($assetsUrl . 'css/mgr/vue-dist/primeicons.min.css'); $this->addCss($assetsUrl . 'css/mgr/vue-dist/product-tabs.min.css'); + // Uppy styles for Gallery tab (GalleryUploader is in a separate chunk, its CSS must be loaded explicitly) + $this->addCss($assetsUrl . 'css/mgr/vue-dist/GalleryUploader.min.css'); $this->addVueModule($assetsUrl . 'js/mgr/vue-dist/product-tabs.min.js'); $show_gallery = $this->getOption('ms3_product_tab_gallery', null, true); - if ($show_gallery) { - $this->addCss($assetsUrl . 'css/mgr/vue-dist/gallery-uploader.min.css'); - // Vue module with VueTools dependency check - $this->addVueModule($assetsUrl . 'js/mgr/vue-dist/gallery-uploader.min.js'); - $this->addLastJavascript($assetsUrl . 'js/mgr/misc/ext.ddview.js'); - $this->addLastJavascript($assetsUrl . 'js/mgr/product/gallery/gallery.panel.js'); - $this->addLastJavascript($assetsUrl . 'js/mgr/product/gallery/gallery.toolbar.js'); - $this->addLastJavascript($assetsUrl . 'js/mgr/product/gallery/gallery.view.js'); - $this->addLastJavascript($assetsUrl . 'js/mgr/product/gallery/gallery.window.js'); - } // Customizable product fields feature $product_fields = array_merge($this->resource->getAllFieldsNames(), ['syncsite']); @@ -121,6 +113,7 @@ public function loadCustomCssJs() 'show_links' => (bool)$this->getOption('ms3_product_tab_links', null, true), 'show_categories' => (bool)$this->getOption('ms3_product_tab_categories', null, true), 'default_thumb' => $this->ms3->config['defaultThumb'], + 'sources' => $this->getMediaSourcesList(), 'main_fields' => $product_main_fields, 'extra_fields' => $product_extra_fields, 'option_keys' => $product_option_keys, @@ -205,6 +198,25 @@ public function prepareFields() } } + /** + * Get list of media sources for gallery source selector + * + * @return array List of [id => int, name => string] + */ + public function getMediaSourcesList() + { + $list = []; + $sources = $this->modx->getCollection('sources.modMediaSource', ['id:>' => 0]); + /** @var \MODX\Revolution\Sources\modMediaSource $source */ + foreach ($sources as $source) { + $list[] = [ + 'id' => (int)$source->get('id'), + 'name' => $source->get('name') ?: ('Source ' . $source->get('id')), + ]; + } + return $list; + } + /** * Load media source properties * diff --git a/core/components/minishop3/lexicon/en/product.inc.php b/core/components/minishop3/lexicon/en/product.inc.php index 4b64a40d..9903ad46 100644 --- a/core/components/minishop3/lexicon/en/product.inc.php +++ b/core/components/minishop3/lexicon/en/product.inc.php @@ -135,6 +135,7 @@ $_lang['ms3_product_selected_undelete'] = 'Restore Selected'; $_lang['ms3_gallery_emptymsg'] = '

No files found.

You can upload them by dragging directly to this panel or by clicking the button above.

'; +$_lang['ms3_gallery_empty_text'] = 'No files yet. Drag files into the upload area above or click «Select files».'; $_lang['ms3_gallery_unavailablemsg'] = 'To upload files to Gallery, you need to create (save) the product first.'; $_lang['ms3_gallery_file_name'] = 'File Name'; $_lang['ms3_gallery_file_title'] = 'Title'; @@ -156,7 +157,7 @@ $_lang['ms3_gallery_file_delete_all'] = 'Delete All'; $_lang['ms3_gallery_file_delete_confirm'] = 'Are you sure you want to delete this file with all its thumbnails?
This operation is irreversible.'; $_lang['ms3_gallery_file_delete_multiple'] = 'Delete Files'; -$_lang['ms3_gallery_file_delete_multiple_confirm'] = 'Are you sure you want to delete these files with all their thumbnails?
This operation is irreversible.'; +$_lang['ms3_gallery_file_delete_multiple_confirm'] = 'Are you sure you want to delete these files with all their thumbnails? This operation is irreversible.'; $_lang['ms3_gallery_errors'] = 'Upload Errors'; @@ -167,6 +168,7 @@ $_lang['ms3_gallery_uppy_browse_folders'] = 'browse folders'; $_lang['ms3_gallery_uppy_upload_complete'] = 'Upload complete'; $_lang['ms3_gallery_uppy_upload_failed'] = 'Upload failed'; +$_lang['ms3_gallery_uppy_failed_to_upload'] = 'Failed to upload %{file}'; $_lang['ms3_gallery_uppy_uploading'] = 'Uploading...'; $_lang['ms3_gallery_uppy_complete'] = 'Complete'; $_lang['ms3_gallery_uppy_cancel'] = 'Cancel'; @@ -181,6 +183,50 @@ $_lang['ms3_gallery_uppy_upload_x_files_1'] = 'Upload %{smart_count} files'; $_lang['ms3_gallery_uppy_upload_x_files_2'] = 'Upload %{smart_count} files'; $_lang['ms3_gallery_uppy_note_max_size'] = 'Maximum size: %{maxSize}'; +$_lang['ms3_gallery_uppy_back'] = 'Back'; +$_lang['ms3_gallery_uppy_add_more_files'] = 'Add more files'; +$_lang['ms3_gallery_uppy_adding_more'] = 'Adding more files'; +$_lang['ms3_gallery_uppy_duplicate_file'] = "Cannot add the duplicate file '%{fileName}', it already exists"; +$_lang['ms3_gallery_uppy_restrictions_failed'] = '%{count} additional restrictions were not fulfilled'; +$_lang['ms3_gallery_uppy_done'] = 'Done'; +$_lang['ms3_gallery_uppy_upload'] = 'Upload'; +$_lang['ms3_gallery_uppy_pause'] = 'Pause'; +$_lang['ms3_gallery_uppy_resume'] = 'Resume'; +$_lang['ms3_gallery_uppy_paused'] = 'Paused'; +$_lang['ms3_gallery_uppy_save'] = 'Save'; +$_lang['ms3_gallery_uppy_save_changes'] = 'Save changes'; +$_lang['ms3_gallery_uppy_finish_editing_file'] = 'Finish editing file'; +$_lang['ms3_gallery_uppy_editing'] = 'Editing %{file}'; +$_lang['ms3_gallery_uppy_remove_file'] = 'Remove file'; +$_lang['ms3_gallery_uppy_edit_file'] = 'Edit file'; +$_lang['ms3_gallery_uppy_edit_image'] = 'Edit image'; +$_lang['ms3_gallery_uppy_drop_hint'] = 'Drop your files here'; +$_lang['ms3_gallery_uppy_error'] = 'Error'; +$_lang['ms3_gallery_uppy_show_error_details'] = 'Show error details'; +$_lang['ms3_gallery_uppy_upload_paused'] = 'Upload paused'; +$_lang['ms3_gallery_uppy_resume_upload'] = 'Resume upload'; +$_lang['ms3_gallery_uppy_pause_upload'] = 'Pause upload'; +$_lang['ms3_gallery_uppy_retry_upload'] = 'Retry upload'; +$_lang['ms3_gallery_uppy_cancel_upload'] = 'Cancel upload'; +$_lang['ms3_gallery_uppy_uploading_x_files_0'] = 'Uploading %{smart_count} file'; +$_lang['ms3_gallery_uppy_uploading_x_files_1'] = 'Uploading %{smart_count} files'; +$_lang['ms3_gallery_uppy_processing_x_files_0'] = 'Processing %{smart_count} file'; +$_lang['ms3_gallery_uppy_processing_x_files_1'] = 'Processing %{smart_count} files'; +$_lang['ms3_gallery_uppy_powered_by'] = 'Powered by %{uppy}'; +$_lang['ms3_gallery_uppy_files_uploaded_of_total_0'] = '%{complete} of %{smart_count} file uploaded'; +$_lang['ms3_gallery_uppy_files_uploaded_of_total_1'] = '%{complete} of %{smart_count} files uploaded'; +$_lang['ms3_gallery_uppy_data_uploaded_of_total'] = '%{complete} of %{total}'; +$_lang['ms3_gallery_uppy_data_uploaded_of_unknown'] = '%{complete} of unknown'; +$_lang['ms3_gallery_uppy_x_time_left'] = '%{time} left'; +$_lang['ms3_gallery_uppy_upload_x_new_files_0'] = 'Upload +%{smart_count} file'; +$_lang['ms3_gallery_uppy_upload_x_new_files_1'] = 'Upload +%{smart_count} files'; +$_lang['ms3_gallery_uppy_x_more_files_added_0'] = '%{smart_count} more file added'; +$_lang['ms3_gallery_uppy_x_more_files_added_1'] = '%{smart_count} more files added'; +$_lang['ms3_gallery_uppy_close_modal'] = 'Close Modal'; +$_lang['ms3_gallery_uppy_response_error'] = 'Server returned an invalid response. Check server logs for PHP errors.'; +$_lang['ms3_gallery_uppy_dashboard_title'] = 'Uppy Dashboard'; +$_lang['ms3_gallery_search_placeholder'] = 'Search by file name...'; +$_lang['ms3_gallery_drag_hint'] = 'Drag to reorder'; $_lang['ms3_product_data_vue'] = 'Product Data (Vue)'; diff --git a/core/components/minishop3/lexicon/ru/product.inc.php b/core/components/minishop3/lexicon/ru/product.inc.php index 2b883191..efc28d83 100644 --- a/core/components/minishop3/lexicon/ru/product.inc.php +++ b/core/components/minishop3/lexicon/ru/product.inc.php @@ -137,6 +137,7 @@ //$_lang['ms3_disabled_while_creating'] = 'Эта функция отключена при создании нового товара.'; $_lang['ms3_gallery_emptymsg'] = '

Файлов не найдено.

Вы можете загрузить их, перетащив прямо на эту панель или выбрав кнопкой вверху.

'; +$_lang['ms3_gallery_empty_text'] = 'Файлов нет. Перетащите файлы в область загрузки выше или нажмите «Выбрать файлы».'; $_lang['ms3_gallery_unavailablemsg'] = 'Для загрузки файлов в Галерею необходимо сначала создать (сохранить) товар.'; $_lang['ms3_gallery_file_name'] = 'Имя файла'; $_lang['ms3_gallery_file_title'] = 'Название'; @@ -158,7 +159,7 @@ $_lang['ms3_gallery_file_delete_all'] = 'Удалить все'; $_lang['ms3_gallery_file_delete_confirm'] = 'Вы действительно хотите удалить этот файл вместе со всеми его уменьшенными копиями?
Эта операция необратима.'; $_lang['ms3_gallery_file_delete_multiple'] = 'Удалить файлы'; -$_lang['ms3_gallery_file_delete_multiple_confirm'] = 'Вы действительно хотите удалить эти файлы со всеми их уменьшенными копиями?
Эта операция необратима.'; +$_lang['ms3_gallery_file_delete_multiple_confirm'] = 'Вы действительно хотите удалить эти файлы со всеми их уменьшенными копиями? Эта операция необратима.'; $_lang['ms3_gallery_errors'] = 'Ошибки при загрузке'; @@ -169,6 +170,7 @@ $_lang['ms3_gallery_uppy_browse_folders'] = 'выбрать папки'; $_lang['ms3_gallery_uppy_upload_complete'] = 'Загрузка завершена'; $_lang['ms3_gallery_uppy_upload_failed'] = 'Ошибка загрузки'; +$_lang['ms3_gallery_uppy_failed_to_upload'] = 'Ошибка загрузки %{file}'; $_lang['ms3_gallery_uppy_uploading'] = 'Загрузка...'; $_lang['ms3_gallery_uppy_complete'] = 'Готово'; $_lang['ms3_gallery_uppy_cancel'] = 'Отмена'; @@ -183,5 +185,49 @@ $_lang['ms3_gallery_uppy_upload_x_files_1'] = 'Загрузить %{smart_count} файла'; $_lang['ms3_gallery_uppy_upload_x_files_2'] = 'Загрузить %{smart_count} файлов'; $_lang['ms3_gallery_uppy_note_max_size'] = 'Макс. размер: %{maxSize}'; +$_lang['ms3_gallery_uppy_back'] = 'Назад'; +$_lang['ms3_gallery_uppy_add_more_files'] = 'Добавить файлы'; +$_lang['ms3_gallery_uppy_adding_more'] = 'Добавление файлов'; +$_lang['ms3_gallery_uppy_duplicate_file'] = "Нельзя добавить файл «%{fileName}» — он уже добавлен"; +$_lang['ms3_gallery_uppy_restrictions_failed'] = 'Ещё %{count} ограничений не выполнено'; +$_lang['ms3_gallery_uppy_done'] = 'Готово'; +$_lang['ms3_gallery_uppy_upload'] = 'Загрузить'; +$_lang['ms3_gallery_uppy_pause'] = 'Пауза'; +$_lang['ms3_gallery_uppy_resume'] = 'Продолжить'; +$_lang['ms3_gallery_uppy_paused'] = 'На паузе'; +$_lang['ms3_gallery_uppy_save'] = 'Сохранить'; +$_lang['ms3_gallery_uppy_save_changes'] = 'Сохранить изменения'; +$_lang['ms3_gallery_uppy_finish_editing_file'] = 'Завершить редактирование'; +$_lang['ms3_gallery_uppy_editing'] = 'Редактирование %{file}'; +$_lang['ms3_gallery_uppy_remove_file'] = 'Удалить файл'; +$_lang['ms3_gallery_uppy_edit_file'] = 'Редактировать файл'; +$_lang['ms3_gallery_uppy_edit_image'] = 'Редактировать изображение'; +$_lang['ms3_gallery_uppy_drop_hint'] = 'Перетащите файлы сюда'; +$_lang['ms3_gallery_uppy_error'] = 'Ошибка'; +$_lang['ms3_gallery_uppy_show_error_details'] = 'Показать детали ошибки'; +$_lang['ms3_gallery_uppy_upload_paused'] = 'Загрузка на паузе'; +$_lang['ms3_gallery_uppy_resume_upload'] = 'Продолжить загрузку'; +$_lang['ms3_gallery_uppy_pause_upload'] = 'Приостановить загрузку'; +$_lang['ms3_gallery_uppy_retry_upload'] = 'Повторить загрузку'; +$_lang['ms3_gallery_uppy_cancel_upload'] = 'Отменить загрузку'; +$_lang['ms3_gallery_uppy_uploading_x_files_0'] = 'Загрузка %{smart_count} файла'; +$_lang['ms3_gallery_uppy_uploading_x_files_1'] = 'Загрузка %{smart_count} файлов'; +$_lang['ms3_gallery_uppy_processing_x_files_0'] = 'Обработка %{smart_count} файла'; +$_lang['ms3_gallery_uppy_processing_x_files_1'] = 'Обработка %{smart_count} файлов'; +$_lang['ms3_gallery_uppy_powered_by'] = 'На базе %{uppy}'; +$_lang['ms3_gallery_uppy_files_uploaded_of_total_0'] = 'Загружено %{complete} из %{smart_count} файла'; +$_lang['ms3_gallery_uppy_files_uploaded_of_total_1'] = 'Загружено %{complete} из %{smart_count} файлов'; +$_lang['ms3_gallery_uppy_data_uploaded_of_total'] = '%{complete} из %{total}'; +$_lang['ms3_gallery_uppy_data_uploaded_of_unknown'] = '%{complete} из неизвестного объёма'; +$_lang['ms3_gallery_uppy_x_time_left'] = 'Осталось %{time}'; +$_lang['ms3_gallery_uppy_upload_x_new_files_0'] = 'Загрузить +%{smart_count} файл'; +$_lang['ms3_gallery_uppy_upload_x_new_files_1'] = 'Загрузить +%{smart_count} файлов'; +$_lang['ms3_gallery_uppy_x_more_files_added_0'] = 'Добавлен ещё %{smart_count} файл'; +$_lang['ms3_gallery_uppy_x_more_files_added_1'] = 'Добавлено ещё %{smart_count} файлов'; +$_lang['ms3_gallery_uppy_close_modal'] = 'Закрыть'; +$_lang['ms3_gallery_uppy_response_error'] = 'Сервер вернул некорректный ответ. Проверьте логи PHP на сервере.'; +$_lang['ms3_gallery_uppy_dashboard_title'] = 'Панель загрузки'; +$_lang['ms3_gallery_search_placeholder'] = 'Поиск по имени файла...'; +$_lang['ms3_gallery_drag_hint'] = 'Перетащите для изменения порядка'; $_lang['ms3_product_data_vue'] = 'Данные товара (Vue)'; diff --git a/core/components/minishop3/src/Processors/Gallery/Upload.php b/core/components/minishop3/src/Processors/Gallery/Upload.php index 9d9915e6..56d7a563 100644 --- a/core/components/minishop3/src/Processors/Gallery/Upload.php +++ b/core/components/minishop3/src/Processors/Gallery/Upload.php @@ -175,6 +175,11 @@ public function process() } } + $this->modx->log( + modX::LOG_LEVEL_DEBUG, + '[ms3Gallery] Upload success, file id=' . $uploaded_file->get('id') + ); + return $this->success('', $uploaded_file); } else { return $this->failure($this->modx->lexicon('ms3_err_gallery_save') . ': ' . diff --git a/vueManager/src/components/ProductData.vue b/vueManager/src/components/ProductData.vue index 8c7ffeb9..549db54b 100644 --- a/vueManager/src/components/ProductData.vue +++ b/vueManager/src/components/ProductData.vue @@ -1,11 +1,11 @@ + + + + + + + diff --git a/vueManager/src/components/product/ProductGalleryEditDialog.vue b/vueManager/src/components/product/ProductGalleryEditDialog.vue new file mode 100644 index 00000000..b1371132 --- /dev/null +++ b/vueManager/src/components/product/ProductGalleryEditDialog.vue @@ -0,0 +1,146 @@ + + +