diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index 41c9f4a47f..9a761cd6b4 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -25,6 +25,6 @@ jobs: path-to-signatures: 'signatures/version1/cla.json' path-to-document: 'https://github.com/coreshop/coreshop/blob/4.0/CLA.md' branch: "main" - allowlist: user1,bot* + allowlist: user1,bot*,Copilot remote-organization-name: "coreshop" remote-repository-name: "cla" diff --git a/CHANGELOG-4.1.x.md b/CHANGELOG-4.1.x.md index a42a6fcb10..3dcaef7c56 100644 --- a/CHANGELOG-4.1.x.md +++ b/CHANGELOG-4.1.x.md @@ -1,3 +1,6 @@ +## 4.1.9 +* Fix Injection in CustomerTransformerController by @dpfaffenbauer in https://github.com/coreshop/CoreShop/pull/2945 + ## 4.1.8 * [Messenger] dispatch `FailedMessageDetailsEvent` to allow customization of failed message details generation by @jdreesen in https://github.com/coreshop/CoreShop/pull/2911 * [Messenger] wrap failed message details info modal data in `
` tags by @jdreesen in https://github.com/coreshop/CoreShop/pull/2910
diff --git a/docs/03_Bundles/Menu_Bundle.md b/docs/03_Bundles/Menu_Bundle.md
index 5277296c8a..8f06ef70e0 100644
--- a/docs/03_Bundles/Menu_Bundle.md
+++ b/docs/03_Bundles/Menu_Bundle.md
@@ -75,14 +75,14 @@ public function registerBundlesToCollection(BundleCollection $collection)
 
    ```javascript
     new coreshop.menu.coreshop.my_menu();
-   
-    pimcore.eventDispatcher.registerTarget('coreshopMenuOpen', new (Class.create({
-        coreshopMenuOpen: function(type, item) {
+
+    document.addEventListener(coreshop.events.menu.open, (e) => {
+        var item = e.detail.item;
+
         if (item.id === 'my-menu-item') {
             alert('My Menu Item has been clicked');
         }
-    }
-
+    });
    ```
 
 
diff --git a/docs/03_Development/02_Localization/03_States/02_Setup_Command.md b/docs/03_Development/02_Localization/03_States/02_Setup_Command.md
new file mode 100644
index 0000000000..00d3fe789a
--- /dev/null
+++ b/docs/03_Development/02_Localization/03_States/02_Setup_Command.md
@@ -0,0 +1,66 @@
+# Setup States Command
+
+CoreShop provides a CLI command to create states/regions for countries after the initial installation.
+
+By default, CoreShop only creates states for Austria (AT) during installation. Use this command to add states for additional countries.
+
+## Usage
+
+```bash
+# Setup states for a single country
+php bin/console coreshop:setup:states DE
+
+# Setup states for multiple countries
+php bin/console coreshop:setup:states DE,US,FR
+
+# Setup states and activate the country if not already active
+php bin/console coreshop:setup:states DE --activate-country
+
+# Verbose output to see individual states being created
+php bin/console coreshop:setup:states DE -v
+```
+
+## Arguments
+
+| Argument | Description |
+|----------|-------------|
+| countries | Comma-separated list of country ISO codes (e.g., DE,US,FR) |
+
+## Options
+
+| Option | Description |
+|--------|-------------|
+| --activate-country | Also activate the country if not already active |
+
+## Prerequisites
+
+Before running this command, ensure that:
+
+1. CoreShop is installed (`coreshop:install` has been run)
+2. The country fixtures have been loaded (countries exist in the database)
+
+## How It Works
+
+The command:
+
+1. Looks up each specified country in the database by ISO code
+2. Loads the country's divisions (states/regions) from the Rinvex country data library
+3. Creates states for each division that doesn't already exist
+4. Optionally activates the country if `--activate-country` is specified
+
+## Example Output
+
+```
+CoreShop States Setup
+=====================
+
+Setting up states for countries: DE
+
+Processing country: DE
+-----------------------
+
+ [OK] Countries processed: 1
+      States created: 16
+      States skipped (already exist): 0
+      Countries activated: 0
+```
diff --git a/src/CoreShop/Bundle/CoreBundle/Command/SetupStatesCommand.php b/src/CoreShop/Bundle/CoreBundle/Command/SetupStatesCommand.php
new file mode 100644
index 0000000000..c3945996f6
--- /dev/null
+++ b/src/CoreShop/Bundle/CoreBundle/Command/SetupStatesCommand.php
@@ -0,0 +1,182 @@
+setName('coreshop:setup:states')
+            ->setDescription('Create states/regions for specified countries.')
+            ->addArgument(
+                'countries',
+                InputArgument::REQUIRED,
+                'Comma-separated list of country ISO codes (e.g., DE,US,FR)',
+            )
+            ->addOption(
+                'activate-country',
+                null,
+                InputOption::VALUE_NONE,
+                'Also activate the country if not already active',
+            )
+            ->setHelp(
+                <<%command.name% command creates states/regions for specified countries.
+
+Examples:
+  php bin/console %command.name% DE
+  php bin/console %command.name% DE,US,FR
+  php bin/console %command.name% DE --activate-country
+EOT
+            )
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        $countriesArg = $input->getArgument('countries');
+        $activateCountry = $input->getOption('activate-country');
+        $countryCodes = array_map('strtoupper', array_map('trim', explode(',', $countriesArg)));
+
+        $languages = Tool::getValidLanguages();
+
+        $io->title('CoreShop States Setup');
+        $io->writeln(sprintf('Setting up states for countries: %s', implode(', ', $countryCodes)));
+
+        $createdStates = 0;
+        $skippedStates = 0;
+        $countriesProcessed = 0;
+        $activatedCountries = 0;
+
+        foreach ($countryCodes as $countryCode) {
+            $io->section(sprintf('Processing country: %s', $countryCode));
+
+            // Check if country exists in database
+            $country = $this->countryRepository->findByCode($countryCode);
+            if ($country === null) {
+                $io->warning(sprintf('Country with code "%s" not found in database. Run fixtures first or install CoreShop.', $countryCode));
+
+                continue;
+            }
+
+            // Load country data from Rinvex
+            try {
+                $rinvexCountry = CountryLoader::country($countryCode);
+            } catch (\Exception $e) {
+                $io->warning(sprintf('Country data not found for code "%s" in Rinvex data: %s', $countryCode, $e->getMessage()));
+
+                continue;
+            }
+
+            // Activate country if requested
+            if ($activateCountry && !$country->getActive()) {
+                $country->setActive(true);
+                $this->entityManager->persist($country);
+                $activatedCountries++;
+                $io->writeln(sprintf('  Activated country: %s', $countryCode));
+            }
+
+            // Get divisions (states/regions)
+            $divisions = $rinvexCountry->getDivisions();
+
+            if (!is_array($divisions) || empty($divisions)) {
+                $io->writeln(sprintf('  No divisions/states found for %s', $countryCode));
+                $countriesProcessed++;
+
+                continue;
+            }
+
+            foreach ($divisions as $isoCode => $division) {
+                if (!$division['name']) {
+                    continue;
+                }
+
+                // Check if state already exists
+                $existingState = $this->stateRepository->findOneBy([
+                    'isoCode' => $isoCode,
+                    'country' => $country,
+                ]);
+
+                if ($existingState !== null) {
+                    $skippedStates++;
+                    $io->writeln(sprintf('  Skipping existing state: %s (%s)', $division['name'], $isoCode), OutputInterface::VERBOSITY_VERBOSE);
+
+                    continue;
+                }
+
+                /**
+                 * @var StateInterface $state
+                 */
+                $state = $this->stateFactory->createNew();
+
+                foreach ($languages as $lang) {
+                    $state->setName($division['name'], $lang);
+                }
+
+                $state->setIsoCode($isoCode);
+                $state->setCountry($country);
+                $state->setActive(true);
+
+                $this->entityManager->persist($state);
+                $createdStates++;
+
+                $io->writeln(sprintf('  Created state: %s (%s)', $division['name'], $isoCode), OutputInterface::VERBOSITY_VERBOSE);
+            }
+
+            $countriesProcessed++;
+        }
+
+        $this->entityManager->flush();
+
+        $io->success([
+            sprintf('Countries processed: %d', $countriesProcessed),
+            sprintf('States created: %d', $createdStates),
+            sprintf('States skipped (already exist): %d', $skippedStates),
+            sprintf('Countries activated: %d', $activatedCountries),
+        ]);
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/commands.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/commands.yml
index 510cd6f980..a42f6c30c3 100644
--- a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/commands.yml
+++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/commands.yml
@@ -50,3 +50,12 @@ services:
         tags:
             - { name: console.command, command: coreshop:migration:generate }
 
+    CoreShop\Bundle\CoreBundle\Command\SetupStatesCommand:
+        arguments:
+            - '@coreshop.repository.country'
+            - '@coreshop.repository.state'
+            - '@coreshop.factory.state'
+            - '@doctrine.orm.entity_manager'
+        tags:
+            - { name: console.command, command: coreshop:setup:states }
+
diff --git a/src/CoreShop/Bundle/MessengerBundle/Resources/public/pimcore/js/list.js b/src/CoreShop/Bundle/MessengerBundle/Resources/public/pimcore/js/list.js
index e275cfb3c3..d27530d6ae 100644
--- a/src/CoreShop/Bundle/MessengerBundle/Resources/public/pimcore/js/list.js
+++ b/src/CoreShop/Bundle/MessengerBundle/Resources/public/pimcore/js/list.js
@@ -21,6 +21,10 @@ coreshop.messenger.list = Class.create({
     messagesStore: null,
     failedMessagesStore: null,
 
+    autoRefreshInterval: null,
+    autoRefreshTimer: null,
+    lastRefreshLabel: null,
+
     initialize: function () {
         this.getPanel();
     },
@@ -37,8 +41,47 @@ coreshop.messenger.list = Class.create({
         }
     },
 
+    updateLastRefreshLabel: function () {
+        if (this.lastRefreshLabel) {
+            var now = new Date();
+            var timeString = Ext.Date.format(now, 'Y-m-d H:i:s');
+            this.lastRefreshLabel.setText(t('coreshop_messenger_last_refresh') + ': ' + timeString);
+        }
+    },
+
+    startAutoRefresh: function (interval) {
+        this.stopAutoRefresh();
+
+        if (interval > 0) {
+            this.autoRefreshInterval = interval;
+            this.autoRefreshTimer = setInterval(this.reload.bind(this), interval * 1000);
+        }
+    },
+
+    stopAutoRefresh: function () {
+        if (this.autoRefreshTimer) {
+            clearInterval(this.autoRefreshTimer);
+            this.autoRefreshTimer = null;
+        }
+        this.autoRefreshInterval = null;
+    },
+
     getPanel: function () {
         if (!this.panel) {
+            this.lastRefreshLabel = Ext.create('Ext.toolbar.TextItem', {
+                text: ''
+            });
+
+            var autoRefreshStore = Ext.create('Ext.data.Store', {
+                fields: ['value', 'text'],
+                data: [
+                    {value: 0, text: t('coreshop_messenger_auto_refresh_disabled')},
+                    {value: 5, text: t('coreshop_messenger_auto_refresh_5s')},
+                    {value: 10, text: t('coreshop_messenger_auto_refresh_10s')},
+                    {value: 30, text: t('coreshop_messenger_auto_refresh_30s')}
+                ]
+            });
+
             this.panel = Ext.create('Ext.panel.Panel', {
                 id: 'coreshop_messenger_list',
                 title: t('coreshop_messenger_list'),
@@ -50,7 +93,21 @@ coreshop.messenger.list = Class.create({
                     xtype: 'button',
                     iconCls: 'pimcore_icon_reload',
                     handler: this.reload.bind(this)
-                }],
+                }, '-', {
+                    xtype: 'combo',
+                    store: autoRefreshStore,
+                    displayField: 'text',
+                    valueField: 'value',
+                    value: 0,
+                    editable: false,
+                    width: 200,
+                    queryMode: 'local',
+                    listeners: {
+                        select: function (combo, record) {
+                            this.startAutoRefresh(record.get('value'));
+                        }.bind(this)
+                    }
+                }, '->', this.lastRefreshLabel],
                 items: [{
                     xtype: 'panel',
                     layout: 'border',
@@ -79,6 +136,7 @@ coreshop.messenger.list = Class.create({
             tabPanel.setActiveItem('coreshop_messenger_list');
 
             this.panel.on('destroy', function () {
+                this.stopAutoRefresh();
                 pimcore.globalmanager.remove('coreshop_messenger_list');
             }.bind(this));
 
@@ -99,7 +157,10 @@ coreshop.messenger.list = Class.create({
                     rootProperty: 'data'
                 }
             },
-            fields: ['name', 'count']
+            fields: ['receiver', 'count'],
+            listeners: {
+                load: this.updateLastRefreshLabel.bind(this)
+            }
         });
         this.chartStore.load();
 
@@ -118,16 +179,23 @@ coreshop.messenger.list = Class.create({
                 type: 'category',
                 position: 'bottom',
                 grid: true,
-                fields: ['receiver'],
+                fields: ['receiver']
             }],
             series: [{
                 type: 'bar',
                 title: 'Messages',
                 xField: 'receiver',
                 yField: 'count',
+                highlight: true,
                 label: {
                     field: 'count',
                     display: 'insideEnd'
+                },
+                tooltip: {
+                    trackMouse: true,
+                    renderer: function (tooltip, record) {
+                        tooltip.setHtml(record.get('receiver') + ': ' + record.get('count') + ' ' + t('coreshop_messenger_messages'));
+                    }
                 }
             }]
         };
diff --git a/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.de.yml b/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.de.yml
index 94ebf33b84..1d96fd740a 100644
--- a/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.de.yml
+++ b/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.de.yml
@@ -10,4 +10,10 @@ coreshop_messenger_failed_messages: 'Fehlgeschlagene Nachrichten'
 coreshop_messenger_pending_messages: 'Ausstehende Nachrichten'
 coreshop_messenger_receivers: 'Empfänger'
 coreshop_messenger_info: 'Details'
-coreshop_permission_messenger: 'CoreShop: Messenger'
\ No newline at end of file
+coreshop_permission_messenger: 'CoreShop: Messenger'
+coreshop_messenger_last_refresh: 'Aktualisiert'
+coreshop_messenger_auto_refresh_disabled: 'Nicht aktualisieren'
+coreshop_messenger_auto_refresh_5s: 'Alle 5 Sekunden aktualisieren'
+coreshop_messenger_auto_refresh_10s: 'Alle 10 Sekunden aktualisieren'
+coreshop_messenger_auto_refresh_30s: 'Alle 30 Sekunden aktualisieren'
+coreshop_messenger_messages: 'Nachricht(en)'
\ No newline at end of file
diff --git a/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.en.yml b/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.en.yml
index 5c1f54acf2..8358c63817 100644
--- a/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.en.yml
+++ b/src/CoreShop/Bundle/MessengerBundle/Resources/translations/admin.en.yml
@@ -10,4 +10,10 @@ coreshop_messenger_failed_messages: 'Failed Messages'
 coreshop_messenger_pending_messages: 'Pending Messages'
 coreshop_messenger_receivers: 'Receivers'
 coreshop_messenger_info: 'Details'
-coreshop_permission_messenger: 'CoreShop: Messenger'
\ No newline at end of file
+coreshop_permission_messenger: 'CoreShop: Messenger'
+coreshop_messenger_last_refresh: 'Refreshed'
+coreshop_messenger_auto_refresh_disabled: 'Do not refresh'
+coreshop_messenger_auto_refresh_5s: 'Refresh every 5 seconds'
+coreshop_messenger_auto_refresh_10s: 'Refresh every 10 seconds'
+coreshop_messenger_auto_refresh_30s: 'Refresh every 30 seconds'
+coreshop_messenger_messages: 'message(s)'
\ No newline at end of file
diff --git a/src/CoreShop/Bundle/OrderBundle/Controller/OrderInvoiceController.php b/src/CoreShop/Bundle/OrderBundle/Controller/OrderInvoiceController.php
index 7b54fc1d48..50d063f68b 100644
--- a/src/CoreShop/Bundle/OrderBundle/Controller/OrderInvoiceController.php
+++ b/src/CoreShop/Bundle/OrderBundle/Controller/OrderInvoiceController.php
@@ -186,8 +186,7 @@ public function renderAction(Request $request): Response
                     'Content-Disposition' => 'inline; filename="invoice-' . $invoice->getId() . '.pdf"',
                 ];
             } catch (\Exception $e) {
-                $responseData = '' . $e->getMessage() . '
trace: ' . $e->getTraceAsString(); - $header = ['Content-Type' => 'text/html']; + return new Response('An error occurred while rendering the invoice.', 500, ['Content-Type' => 'text/html']); } return new Response($responseData, 200, $header); diff --git a/src/CoreShop/Bundle/OrderBundle/Controller/OrderShipmentController.php b/src/CoreShop/Bundle/OrderBundle/Controller/OrderShipmentController.php index 55de86e161..e370910f25 100644 --- a/src/CoreShop/Bundle/OrderBundle/Controller/OrderShipmentController.php +++ b/src/CoreShop/Bundle/OrderBundle/Controller/OrderShipmentController.php @@ -182,8 +182,7 @@ public function renderAction(Request $request): Response 'Content-Disposition' => 'inline; filename="shipment-' . $shipment->getId() . '.pdf"', ]; } catch (\Exception $e) { - $responseData = '' . $e->getMessage() . '
trace: ' . $e->getTraceAsString(); - $header = ['Content-Type' => 'text/html']; + return new Response('An error occurred while rendering the shipment.', 500, ['Content-Type' => 'text/html']); } return new Response($responseData, 200, $header); diff --git a/src/CoreShop/Bundle/ResourceBundle/Resources/public/pimcore/js/object/objectMultihref.js b/src/CoreShop/Bundle/ResourceBundle/Resources/public/pimcore/js/object/objectMultihref.js index 4642f3741b..9b574712c5 100644 --- a/src/CoreShop/Bundle/ResourceBundle/Resources/public/pimcore/js/object/objectMultihref.js +++ b/src/CoreShop/Bundle/ResourceBundle/Resources/public/pimcore/js/object/objectMultihref.js @@ -154,20 +154,19 @@ coreshop.object.objectMultihref = Class.create(pimcore.object.tags.manyToManyObj return this.component.getEl().dom; }.bind(this), onNodeOver: function (overHtmlNode, ddSource, e, data) { - var record = data.records[0]; - var data = record.data; var fromTree = this.isFromTree(ddSource); - if (data.elementType == 'object' && this.dndAllowed(data, fromTree)) { - return Ext.dd.DropZone.prototype.dropAllowed; - } else { - return Ext.dd.DropZone.prototype.dropNotAllowed; + // Check if any of the records can be dropped + for (var record of data.records) { + var recordData = record.data; + if (recordData.elementType === 'object' && this.dndAllowed(recordData, fromTree)) { + return Ext.dd.DropZone.prototype.dropAllowed; + } } + return Ext.dd.DropZone.prototype.dropNotAllowed; }.bind(this), onNodeDrop: function (target, ddSource, e, data) { - var record = data.records[0]; - var data = record.data; var fromTree = this.isFromTree(ddSource); // check if data is a treenode, if not allow drop because of the reordering @@ -175,24 +174,31 @@ coreshop.object.objectMultihref = Class.create(pimcore.object.tags.manyToManyObj return true; } - if (data.elementType != 'object') { - return false; - } + var addedAny = false; - if (this.dndAllowed(data, fromTree)) { - var initData = { - id: data.id, - path: data.path, - type: data.className - }; + // Process all records in the drag selection + for (var record of data.records) { + var recordData = record.data; + + if (recordData.elementType !== 'object') { + continue; + } - if (!this.objectAlreadyExists(initData.id)) { - this.store.add(initData); - return true; + if (this.dndAllowed(recordData, fromTree)) { + var initData = { + id: recordData.id, + path: recordData.path, + type: recordData.className + }; + + if (!this.objectAlreadyExists(initData.id)) { + this.store.add(initData); + addedAny = true; + } } } - return false; + return addedAny; }.bind(this) }); }.bind(this));