diff --git a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..55f99697c289b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php @@ -0,0 +1,70 @@ +synchronizeContent = $synchronizeContent; + $this->state = $state; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-content:sync'); + $this->setDescription('Synchronize content with assets'); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing content with assets...'); + $this->state->emulateAreaCode( + Area::AREA_ADMINHTML, + function () { + $this->synchronizeContent->execute(); + } + ); + $output->writeln('Completed content synchronization.'); + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/LICENSE.txt b/app/code/Magento/MediaContentSynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php new file mode 100644 index 0000000000000..bcce3514e4ad9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -0,0 +1,37 @@ +synchronize = $synchronize; + } + + /** + * Run media files synchronization. + */ + public function execute() : void + { + $this->synchronize->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php new file mode 100644 index 0000000000000..ad6fdd27d7067 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -0,0 +1,45 @@ +publisher = $publisher; + } + + /** + * Publish media content synchronization message to the message queue. + */ + public function execute() : void + { + $this->publisher->publish( + self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, + [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION] + ); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php new file mode 100644 index 0000000000000..e81817282dcc0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php @@ -0,0 +1,61 @@ +deleteContentAssetLinks = $deleteContentAssetLinks; + $this->getEntities = $getEntities; + $this->getOutdatedRelations = $getOutdatedRelations; + } + + /** + * Remove media content if entity already deleted. + */ + public function execute(): void + { + foreach ($this->getEntities->execute() as $entity) { + $assetsLinks = $this->getOutdatedRelations->execute($entity); + if (!empty($assetsLinks)) { + $this->deleteContentAssetLinks->execute($assetsLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php new file mode 100644 index 0000000000000..37271ce469715 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php @@ -0,0 +1,120 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Returns content asset links wichs entity_id not exist anymore. + * + * @param string $entityType + * @throws CouldNotDeleteException + * @return ContentAssetLinkInterface[] + */ + public function execute(string $entityType): array + { + $contentAssetLinks= []; + try { + $entityData = $this->metadataPool->getMetadata($entityType); + $connection = $this->resourceConnection->getConnection(); + $mediaContentTable = $this->resourceConnection->getTableName(self::MEDIA_CONTENT_ASSET_TABLE); + $select = $connection->select(); + + $select->from(['mca' => $mediaContentTable], ['asset_id', 'entity_id', 'entity_type', 'field']); + $select->joinLeft( + ['et' => $entityData->getEntityTable()], + 'et.' . $entityData->getIdentifierField() . ' = mca.entity_id ', + [$entityData->getIdentifierField() . ' AS entity_identifier'] + ); + $select->where('et.' . $entityData->getIdentifierField() . ' IS NULL'); + $select->where('mca.entity_type = ?', $entityData->getEavEntityType() ?? $entityData->getEntityTable()); + $assets = $connection->fetchAll($select); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException(__('Could not fetch media content links data'), $exception); + } + + foreach ($assets as $asset) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => $asset['entity_type'], + 'entityId' => $asset['entity_id'], + 'field' => $asset['field'] + ] + ); + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset['asset_id'], + 'contentIdentity' => $contentIdentity + ] + ); + } + + return $contentAssetLinks; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..cea8cc6ad44da --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php @@ -0,0 +1,109 @@ +removeObsoleteContent = $removeObsoleteContent; + $this->dateFactory = $dateFactory; + $this->flagManager = $flagManager; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + try { + $synchronizer->execute(); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following content synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + + $this->setLastExecutionTime(); + $this->removeObsoleteContent->execute(); + } + + /** + * Set last synchronizer execution time + */ + private function setLastExecutionTime(): void + { + $currentTime = $this->dateFactory->create()->gmtDate(); + $this->flagManager->saveFlag(self::LAST_EXECUTION_TIME_CODE, $currentTime); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php new file mode 100644 index 0000000000000..e428f7d273bb4 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php @@ -0,0 +1,41 @@ +publish = $publish; + } + + /** + * Publish content synchronization request message to the queue. + * + * @param Consume $subject + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(Consume $subject): void + { + $this->publish->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/README.md b/app/code/Magento/MediaContentSynchronization/README.md new file mode 100644 index 0000000000000..69098ab02eb0b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaContentSynchronization module + +The Magento_MediaContentSynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronization module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json new file mode 100644 index 0000000000000..3be5f535487ec --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-media-content-synchronization", + "description": "Magento module provides implementation of the media content data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/framework-message-queue": "*", + "magento/module-media-content-api": "*" + }, + "suggest": { + "magento/module-media-gallery-synchronization": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml new file mode 100644 index 0000000000000..e3436aee85331 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml new file mode 100644 index 0000000000000..d4615c15206e5 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -0,0 +1,21 @@ + + + + + + + + Magento\MediaContentSynchronization\Console\Command\Synchronize + + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/module.xml b/app/code/Magento/MediaContentSynchronization/etc/module.xml new file mode 100644 index 0000000000000..7f04d9b57d8a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..6a141c04c59a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9751d1161b2f2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..4dc43ef1ac13f --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/code/Magento/MediaContentSynchronization/registration.php b/app/code/Magento/MediaContentSynchronization/registration.php new file mode 100644 index 0000000000000..a157f7ec90a6a --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/registration.php @@ -0,0 +1,14 @@ +" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php new file mode 100644 index 0000000000000..38129b2b1c6b9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php @@ -0,0 +1,38 @@ +entities = $entities; + } + + /** + * Get all entities configuration used in media content. + * + * @return array + */ + public function execute(): array + { + return $this->entities; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php new file mode 100644 index 0000000000000..ad62ae4136378 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php @@ -0,0 +1,21 @@ +synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizerInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/README.md b/app/code/Magento/MediaContentSynchronizationApi/README.md new file mode 100644 index 0000000000000..25ceae24452f1 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentSynchronizationApi module + +The Magento_MediaContentSynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronizationApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json new file mode 100644 index 0000000000000..1f1e5e4b51c5b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-content-synchronization-api", + "description": "Magento module responsible for the media content synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..76bdd9b1cb162 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..3a149b31da3cb --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationApi/registration.php b/app/code/Magento/MediaContentSynchronizationApi/registration.php new file mode 100644 index 0000000000000..965e31fa45516 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/registration.php @@ -0,0 +1,14 @@ +" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php new file mode 100644 index 0000000000000..665a22b045e44 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php @@ -0,0 +1,112 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [ + self::CATEGORY_IDENTITY_FIELD, + self::CATEGORY_UPDATED_AT_FIELD + ]; + foreach ($this->fetchBatches->execute(self::CATEGORY_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CATEGORY_IDENTITY_FIELD] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php new file mode 100644 index 0000000000000..5d72399752602 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php @@ -0,0 +1,109 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fetchBatches = $fetchBatches; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [self::PRODUCT_TABLE_ENTITY_ID, self::PRODUCT_TABLE_UPDATED_AT_FIELD]; + foreach ($this->fetchBatches->execute(self::PRODUCT_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::PRODUCT_TABLE_ENTITY_ID] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/README.md b/app/code/Magento/MediaContentSynchronizationCatalog/README.md new file mode 100644 index 0000000000000..8395ffc10d4d2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCatalog module + +The Magento_MediaContentCatalog provides the implementation of MediaContentSyncronization functionality for Magento_Catalog module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php new file mode 100644 index 0000000000000..b8f12bad6bd77 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php @@ -0,0 +1,85 @@ +synchronizer = Bootstrap::getObjectManager()->get(Category::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between category and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $categoryId = 28767; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => $categoryId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($categoryId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php new file mode 100644 index 0000000000000..247fdf4a770ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php @@ -0,0 +1,85 @@ +synchronizer = Bootstrap::getObjectManager()->get(Product::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between products and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $productId = 1567; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => $productId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($productId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json new file mode 100644 index 0000000000000..733f29d3a42c2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-catalog", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Catalog module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCatalog\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml new file mode 100644 index 0000000000000..8cc86fde8fbcd --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -0,0 +1,41 @@ + + + + + + + image + description + + + + + + + Magento\Catalog\Api\Data\ProductInterface + Magento\Catalog\Api\Data\CategoryInterface + + + + + + + description + short_description + + + + + + + Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Category + Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml new file mode 100644 index 0000000000000..9660dcb107b45 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/registration.php b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php new file mode 100644 index 0000000000000..1e8b47dc15b50 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php @@ -0,0 +1,14 @@ +" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php new file mode 100644 index 0000000000000..73586c8daf7f3 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php @@ -0,0 +1,107 @@ +contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * Synchronize assets and contents + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_BLOCK_TABLE_ENTITY_ID, + self::CMS_BLOCK_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_BLOCK_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize block entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_BLOCK_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php new file mode 100644 index 0000000000000..dcc855940d157 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php @@ -0,0 +1,107 @@ +fetchBatches = $fetchBatches; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_PAGE_TABLE_ENTITY_ID, + self::CMS_PAGE_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_PAGE_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize page entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_PAGE_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/README.md b/app/code/Magento/MediaContentSynchronizationCms/README.md new file mode 100644 index 0000000000000..58582b1b2d706 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCms module + +The Magento_MediaContentCms provides the implementation of MediaContentSyncronization functionality for Magento_Cms module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php new file mode 100644 index 0000000000000..2737ab524584b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php @@ -0,0 +1,109 @@ +synchronizer = Bootstrap::getObjectManager()->get(Block::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between blocks and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($blockId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * Get fixture block + * + * @param string $identifier + * @return BlockInterface + * @throws LocalizedException + */ + private function getBlock(string $identifier): BlockInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = $objectManager->get(BlockRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier) + ->create(); + + return current($blockRepository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php new file mode 100644 index 0000000000000..1dcbb96dc7914 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php @@ -0,0 +1,109 @@ +synchronizer = Bootstrap::getObjectManager()->get(Page::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between pages and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($pageId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * Get fixture page + * + * @param string $identifier + * @return PageInterface + * @throws LocalizedException + */ + private function getPage(string $identifier): PageInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var PageRepositoryInterface $repository */ + $repository = $objectManager->get(PageRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier) + ->create(); + + return current($repository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/composer.json b/app/code/Magento/MediaContentSynchronizationCms/composer.json new file mode 100644 index 0000000000000..9028b9dacd0a2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-cms", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Cms module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCms\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml new file mode 100644 index 0000000000000..7def330298789 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -0,0 +1,39 @@ + + + + + + + Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block + Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page + + + + + + + Magento\Cms\Api\Data\BlockInterface + Magento\Cms\Api\Data\PageInterface + + + + + + + content + + + + + + + content + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml new file mode 100644 index 0000000000000..58497b81a2174 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaContentSynchronizationCms/registration.php b/app/code/Magento/MediaContentSynchronizationCms/registration.php new file mode 100644 index 0000000000000..13ed4b73f70ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/registration.php @@ -0,0 +1,14 @@ +" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php new file mode 100644 index 0000000000000..d439b53c120cb --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -0,0 +1,109 @@ +deleteAssetsByPaths = $deleteAssetsByPath; + $this->filesystem = $filesystem; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->storage = $storage; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + } + + /** + * Saves base category image information after moving from tmp folder. + * + * @param ImageUploader $subject + * @param string $imagePath + * @return string + * @throws LocalizedException + */ + public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string + { + if (!$this->config->isEnabled()) { + return $imagePath; + } + + $absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath; + $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1); + $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); + + if (!empty($tmpAssets)) { + $this->deleteAssetsByPaths->execute([$tmpAssets[0]->getPath()]); + } + + $this->synchronizeFiles->execute( + [ + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getRelativePath($absolutePath) + ] + ); + + return $imagePath; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/README.md b/app/code/Magento/MediaGalleryCatalogIntegration/README.md new file mode 100644 index 0000000000000..bcb37bd486dab --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryCatalogIntegration + +The purpose of this module is for extending catalog image uploader functionality. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json new file mode 100644 index 0000000000000..efabb70da9f39 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-gallery-catalog-integration", + "description": "Magento module responsible for extending catalog image uploader functionality", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-gallery-ui-api": "*" + }, + "suggest": { + "magento/module-catalog": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..2f8fab34911d6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml new file mode 100644 index 0000000000000..c9f1164121e91 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/registration.php b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php new file mode 100644 index 0000000000000..9495790092df1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php @@ -0,0 +1,14 @@ +resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->getConfig()->getTitle()->prepend(__('Categories')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..e17b02ec40737 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php @@ -0,0 +1,199 @@ +categoryList = $categoryList; + $this->searchResultFactory = $searchResultFactory; + $this->attributeValueFactory = $attributeValueFactory; + $this->documentFactory = $documentFactory; + $this->filterGroupBuilder = $filterGroupBuilder; + } + + /** + * @inheritdoc + */ + public function getData() + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + $searchCriteria = $this->getSearchCriteria(); + $searchCriteria = $this->skipRootCategory($searchCriteria); + $collection = $this->categoryList->getList($searchCriteria); + $items = []; + + foreach ($collection->getItems() as $category) { + $items[] = $this->createDocument( + [ + 'entity_id' => $category->getEntityId(), + 'name' => $category->getName(), + 'image' => $category->getImage(), + 'path' => $category->getPath(), + 'display_mode' => $category->getDisplayMode(), + 'products' => $category->getProductCount(), + 'include_in_menu' => $category->getIncludeInMenu(), + 'is_active' => $category->getIsActive() + ] + ); + } + + $searchResult = $this->searchResultFactory->create(); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($items); + $searchResult->setTotalCount($collection->getTotalCount()); + + return $searchResult; + } + + /** + * Skip empty root category in collection + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchCriteriaInterface + */ + private function skipRootCategory(SearchCriteriaInterface $searchCriteria): SearchCriteriaInterface + { + $filterGroups = $searchCriteria->getFilterGroups(); + + $filters[] = $this->filterBuilder + ->setField(self::ENTITY_ID) + ->setConditionType('neq') + ->setValue(1) + ->create(); + $filterGroups[] = $this->filterGroupBuilder->setFilters($filters)->create(); + $searchCriteria->setFilterGroups($filterGroups); + return $searchCriteria; + } + + /** + * Add attributes to grid result + * + * @param array $attributes [code => value] + */ + private function createDocument(array $attributes): Document + { + $item = $this->documentFactory->create(); + $customAttributes = []; + + foreach ($attributes as $code => $value) { + $attribute = $this->attributeValueFactory->create(); + $attribute->setAttributeCode($code); + $attribute->setValue($value); + $customAttributes[$code] = $attribute; + } + + $item->setCustomAttributes($customAttributes); + + return $item; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md new file mode 100644 index 0000000000000..f47b031875f5d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCatalogUi module + +The Magento_MediaGalleryCatalogUi module that implement category grid for media gallery. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml new file mode 100644 index 0000000000000..0788bbd60291a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + Assert category grid page basic columns values for default category + + + + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml new file mode 100644 index 0000000000000..2444cb314ad22 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + Navigates to category grid page by link. + + + + + + diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml new file mode 100644 index 0000000000000..99cee48f443c7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml @@ -0,0 +1,12 @@ + + + + +
+ + diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml new file mode 100644 index 0000000000000..1f1ce05222e7e --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + +
+
diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml new file mode 100644 index 0000000000000..a495e2ff07e6a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951846"/> + <description value="User filters assets used in categories"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderAgain"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Categories"/> + <argument name="optionName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml new file mode 100644 index 0000000000000..d68fd4cb7cca8 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiUsedInProductFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInProductsFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in products filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <description value="User filters assets used in products"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab"/> + <waitForElementVisible selector="{{CatalogWYSIWYGSection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{CatalogWYSIWYGSection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Products"/> + <argument name="optionName" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml new file mode 100644 index 0000000000000..6b7bd3ba11f45 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User sees category entities where asset is used in"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User sees category entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminAssertCategoryGridPageDetailsActionGroup" stepKey="assertCategoryGridPageRendered"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php new file mode 100644 index 0000000000000..0e7edd53bb45d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\UrlInterface; + +/** + * Class CategoryActions for Category grid + */ +class CategoryActions extends Column +{ + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param UrlInterface $urlBuilder + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + UrlInterface $urlBuilder, + array $components = [], + array $data = [] + ) { + $this->urlBuilder = $urlBuilder; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')]['edit'] = [ + 'href' => $this->urlBuilder->getUrl( + 'catalog/category/edit', + [ + 'id' => $item['entity_id'] + ] + ), + 'label' => __('Edit'), + 'hidden' => false, + ]; + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php new file mode 100644 index 0000000000000..38569f5f698da --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Path column for Category grid + */ +class Path extends Column +{ + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param CategoryRepositoryInterface $categoryRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + CategoryRepositoryInterface $categoryRepository, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->categoryRepository = $categoryRepository; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName] = $this->getCategoryPathWithNames($item[$fieldName]); + } + } + } + + return $dataSource; + } + + /** + * Replace category path ids with category names + * + * @param string $pathWithIds + */ + private function getCategoryPathWithNames(string $pathWithIds): string + { + $categoryPathWithName = ''; + $categoryIds = explode('/', $pathWithIds); + foreach ($categoryIds as $id) { + if ($id == 1) { + continue; + } + $categoryName = $this->categoryRepository->get($id)->getName(); + $categoryPathWithName .= ' / ' . $categoryName; + } + return $categoryPathWithName; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php new file mode 100644 index 0000000000000..efb2ad2f8dae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Helper\Image; +use Magento\Framework\DataObject; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Thumbnail column for Category grid + */ +class Thumbnail extends Column +{ + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Image + */ + private $imageHelper; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param Image $image + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + Image $image, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->imageHelper = $image; + $this->storeManager = $storeManager; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); + } else { + $category = new DataObject($item); + $imageHelper = $this->imageHelper->init($category, 'product_listing_thumbnail'); + $item[$fieldName . '_src'] = $imageHelper->getUrl(); + } + } + } + + return $dataSource; + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl() . $path; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json new file mode 100644 index 0000000000000..985d581beff25 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-media-gallery-catalog-ui", + "description": "Magento module that implement category grid for media gallery.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-store": "*", + "magento/module-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..500ac10f4745a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="product_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product</item> + <item name="category_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_product</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_category</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="catalog_category" xsi:type="array"> + <item name="name" xsi:type="string">Categories</item> + <item name="link" xsi:type="string">media_gallery_catalog/category/index</item> + </item> + <item name="catalog_product" xsi:type="array"> + <item name="name" xsi:type="string">Products</item> + <item name="link" xsi:type="string">catalog/product/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..45f1ccce1c64f --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_catalog" frontName="media_gallery_catalog"> + <module name="Magento_MediaGalleryCatalogUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml new file mode 100644 index 0000000000000..4a593cbf10901 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryCatalogUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/registration.php b/app/code/Magento/MediaGalleryCatalogUi/registration.php new file mode 100644 index 0000000000000..c0376e2a828d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCatalogUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml new file mode 100644 index 0000000000000..1e195efc1beab --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-category-container" name="content"> + <uiComponent name="media_gallery_category_listing"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml new file mode 100644 index 0000000000000..e0b9eacbb4d20 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_category_listing.media_gallery_category_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_category_columns</spinner> + <deps> + <dep>media_gallery_category_listing.media_gallery_category_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_category_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">entity_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryCatalogUi\Model\Listing\DataProvider" name="media_gallery_category_listing_data_source"> + <settings> + <requestFieldName>entity_id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="name" > + <settings> + <placeholder>Search by category name</placeholder> + <label>Name</label> + </settings> + </filterSearch> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">catalog_category</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + </listingToolbar> + <columns name="media_gallery_category_columns"> + <column name="entity_id"> + <settings> + <filter>text</filter> + <label translate="true">ID</label> + </settings> + </column> + <column name="image" component="Magento_Ui/js/grid/columns/thumbnail" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Thumbnail"> + <settings> + <sortable>false</sortable> + <label translate="true">Image</label> + </settings> + </column> + <column name="name"> + <settings> + <label translate="true">Name</label> + </settings> + </column> + <column name="path" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Path"> + <settings> + <label translate="true">Path</label> + </settings> + </column> + <column name="display_mode"> + <settings> + <filter>text</filter> + <label translate="true">Display Mode</label> + </settings> + </column> + <column name="products"> + <settings> + <label translate="true">Products</label> + </settings> + </column> + <column name="include_in_menu" component="Magento_Ui/js/grid/columns/select"> + <argument name="data" xsi:type="array"> + </argument> + <settings> + <options> + <option name="Yes" xsi:type="array"> + <item name="value" xsi:type="number">1</item> + <item name="label" xsi:type="string">Yes</item> + </option> + <option name="No" xsi:type="array"> + <item name="value" xsi:type="number">0</item> + <item name="label" xsi:type="string">No</item> + </option> + </options> + <dataType>select</dataType> + <filter>select</filter> + <label translate="true">In Menu</label> + </settings> + </column> + <column name="is_active" component="Magento_Ui/js/grid/columns/select" > + <settings> + <dataType>select</dataType> + <filter>select</filter> + <options> + <option name="Yes" xsi:type="array"> + <item name="value" xsi:type="number">1</item> + <item name="label" xsi:type="string">Yes</item> + </option> + <option name="No" xsi:type="array"> + <item name="value" xsi:type="number">0</item> + <item name="label" xsi:type="string">No</item> + </option> + </options> + <label translate="true">Enabled</label> + </settings> + </column> + <actionsColumn name="actions" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\CategoryActions" sortOrder="1000"> + <settings> + <indexField>entity_id</indexField> + </settings> + </actionsColumn> + </columns> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..97743b458e8d7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + component="Magento_Catalog/js/components/product-ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> + <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..97743b458e8d7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + component="Magento_Catalog/js/components/product-ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> + <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..0d2a1897e0c25 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,23 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +& when (@media-common = true) { + + .media-gallery-category-container { + + .admin__field-label { + text-align: left; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php new file mode 100644 index 0000000000000..7beb95375073e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Block; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search blocks for ui-select component + */ +class Search extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::block'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param BlockRepositoryInterface $blockRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + BlockRepositoryInterface $blockRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->blockRepository = $blockRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute() : ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->blockRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $block) { + $id = $block->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php new file mode 100644 index 0000000000000..b211e58a0e8c6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Page; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search pages for ui-select component + */ +class Search extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::page'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param PageRepositoryInterface $pageRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + PageRepositoryInterface $pageRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->pageRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $page) { + $id = $page->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md new file mode 100644 index 0000000000000..a5c2eb24c6c15 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCmsUi module + +The Magento_MediaGalleryCmsUi module provides Magento_Cms related UI elements to the media gallery user interface + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml new file mode 100644 index 0000000000000..f0938016d12f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FillOutCustomCMSPageContentActionGroup"> + <annotations> + <description>Fills out the Page details (Page Title, Content and URL Key)</description> + </annotations> + + <arguments> + <argument name="title" type="string"/> + <argument name="content" type="string"/> + <argument name="identifier" type="string"/> + </arguments> + + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{content}}" stepKey="fillFieldContentHeading"/> + <scrollTo selector="{{CmsNewPagePageContentSection.content}}" stepKey="scrollToPageContent"/> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{content}}" stepKey="fillFieldContent"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{identifier}}" stepKey="fillFieldUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml new file mode 100644 index 0000000000000..810d9eea4e261 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCmsUiUsedInBlocksFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInBlocksFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in blocks filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951850"/> + <description value="User filters assets used in blocks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="block" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="block" stepKey="deleteBlock"/> + </after> + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$block$$"/> + </actionGroup> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="saveBlock"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Blocks"/> + <argument name="optionName" value="$$block.title$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml new file mode 100644 index 0000000000000..a6bfdb781a734 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCmsUiUsedInPagesFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInPagesFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in pages filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4934276"/> + <description value="User filters assets used in pages"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> + <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> + <argument name="title" value="Unique page title MediaGalleryUi"/> + <argument name="content" value="MediaGalleryUI content"/> + <argument name="identifier" value="test-page-1"/> + </actionGroup> + + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Pages"/> + <argument name="optionName" value="Unique page title MediaGalleryUi"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + + <actionGroup ref="AdminNavigateToPageGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="AdminSearchCmsPageInGridByUrlKeyActionGroup" stepKey="findCreatedCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json new file mode 100644 index 0000000000000..1ecfb9a3c8855 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-media-gallery-cms-ui", + "description": "Cms related UI elements in the magento media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCmsUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..b06ad0fff1df6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="page_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page</item> + <item name="block_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_page</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_block</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="cms_block" xsi:type="array"> + <item name="name" xsi:type="string">Blocks</item> + <item name="link" xsi:type="string">cms/block/index</item> + </item> + <item name="cms_page" xsi:type="array"> + <item name="name" xsi:type="string">Pages</item> + <item name="link" xsi:type="string">cms/page/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..2dc8b3ade5be7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_cms" frontName="media_gallery_cms"> + <module name="Magento_MediaGalleryCmsUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/module.xml b/app/code/Magento/MediaGalleryCmsUi/etc/module.xml new file mode 100644 index 0000000000000..8a39b8328b387 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryCmsUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/registration.php b/app/code/Magento/MediaGalleryCmsUi/registration.php new file mode 100644 index 0000000000000..0e68935eba590 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCmsUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..509a7e6a53673 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..509a7e6a53673 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php new file mode 100644 index 0000000000000..317b811df5692 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Model; + +use Magento\Framework\DataObject; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Provider to get open media gallery dialog URL for WYSIWYG and widgets + */ +class OpenDialogUrlProvider extends DataObject +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @param ConfigInterface $config + */ + public function __construct(ConfigInterface $config) + { + $this->config = $config; + } + + /** + * Get Url based on media gallery configuration + * + * @return string + */ + public function getUrl(): string + { + return $this->config->isEnabled() ? 'media_gallery/index/index' : 'cms/wysiwyg_images/index'; + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php new file mode 100644 index 0000000000000..fbe35db298b04 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Uploader; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Psr\Log\LoggerInterface; + +/** + * Save image information by SaveAssetsInterface. + */ +class SaveImageInformation +{ + private const IMAGE_FILE_NAME_PATTERN = '#\.(jpg|jpeg|gif|png)$# i'; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @param Filesystem $filesystem + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param SynchronizeFilesInterface $synchronizeFiles + * @param ConfigInterface $config + */ + public function __construct( + Filesystem $filesystem, + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + SynchronizeFilesInterface $synchronizeFiles, + ConfigInterface $config + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->filesystem = $filesystem; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + } + + /** + * Saves asset to media gallery after save image. + * + * @param Uploader $subject + * @param array $result + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Uploader $subject, array $result): array + { + if (!$this->config->isEnabled()) { + return $result; + } + + $path = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath(rtrim($result['path'], '/') . '/' . ltrim($result['file'], '/')); + if (!$this->isApplicable($path)) { + return $result; + } + $this->synchronizeFiles->execute([$path]); + + return $result; + } + + /** + * Can asset be saved with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match(self::IMAGE_FILE_NAME_PATTERN, $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/README.md b/app/code/Magento/MediaGalleryIntegration/README.md new file mode 100644 index 0000000000000..365cde86777f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryIntegration + +The purpose of this module is to keep the integration of enhanced media gallery to Magento separated from implementation. diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php new file mode 100644 index 0000000000000..dfeaa3eff56bd --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Ui\Component\Form\Element\DataType\Media\Image; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class ImageComponentOpenDialogUrlTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Image + */ + private $image; + + /** + * @var string + */ + private $mediaGalleryOpenDialogUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->image = $this->objectManger->create(Image::class); + $this->image->setData('config', ['initialMediaGalleryOpenSubpath' => 'wysiwyg']); + + $url = $this->objectManger->create(UrlInterface::class); + $this->mediaGalleryOpenDialogUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertNotEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php new file mode 100644 index 0000000000000..7a3316f293879 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\MediaGalleryIntegration\Model\OpenDialogUrlProvider; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests cover getting correct url based on the config settings. + * @magentoAppArea adminhtml + */ +class OpenDialogUrlProviderTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var OpenDialogUrlProvider + */ + private $openDialogUrlProvider; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $config = $this->objectManger->create(ConfigInterface::class); + $this->openDialogUrlProvider = $this->objectManger->create( + OpenDialogUrlProvider::class, + ['config' => $config] + ); + } + + /** + * Test getting open dialog url with enhanced media gallery disabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + self::assertEquals('cms/wysiwyg_images/index', $this->openDialogUrlProvider->getUrl()); + } + + /** + * Test getting open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + self::assertEquals('media_gallery/index/index', $this->openDialogUrlProvider->getUrl()); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php new file mode 100644 index 0000000000000..81a4dc642cfa0 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tinymce3\Model\Config\Gallery\Config; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class TinyMceOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Config + */ + private $tinyMce3Config; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $fileBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->tinyMce3Config = $this->objectManger->create(Config::class); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $this->fileBrowserWindowUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertNotEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php new file mode 100644 index 0000000000000..aebf5927869d5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Config; +use Magento\Cms\Model\Wysiwyg\Gallery\DefaultConfigProvider; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update wysiwyg editor dialog url update when media gallery enabled. + * @magentoAppArea adminhtml + */ +class WysiwygDefaultConfigOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $filesBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $imageHelper = $this->objectManger->create(Images::class); + $this->filesBrowserWindowUrl = $url->getUrl( + 'media_gallery/index/index', + ['current_tree_path' => $imageHelper->idEncode(Config::IMAGE_DIRECTORY)] + ); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertNotEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json new file mode 100644 index 0000000000000..c55d6e0b89733 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -0,0 +1,31 @@ +{ + "name": "magento/module-media-gallery-integration", + "description": "Magento module responsible for integration of enhanced media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*" + }, + "require-dev": { + "magento/module-cms": "*" + }, + "suggest": { + "magento/module-catalog": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..d4b4f8988b622 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl"> + <arguments> + <argument name="url" xsi:type="object">Magento\MediaGalleryIntegration\Model\OpenDialogUrlProvider</argument> + </arguments> + </type> + <type name="Magento\Framework\File\Uploader"> + <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/etc/module.xml b/app/code/Magento/MediaGalleryIntegration/etc/module.xml new file mode 100644 index 0000000000000..88af90477cc8a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryIntegration"> + <sequence> + <module name="Magento_Ui"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/registration.php b/app/code/Magento/MediaGalleryIntegration/registration.php new file mode 100644 index 0000000000000..028f8d5b4288a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryIntegration', __DIR__); diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php new file mode 100644 index 0000000000000..9935904468388 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Write iptc data to the file return updated FileInterface with iptc data + */ +class AddIptcMetadata +{ + private const IPTC_TITLE_SEGMENT = '2#005'; + private const IPTC_DESCRIPTION_SEGMENT = '2#120'; + private const IPTC_KEYWORDS_SEGMENT = '2#025'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param DriverInterface $driver + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + DriverInterface $driver, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->driver = $driver; + $this->fileReader = $fileReader; + } + + /** + * Write metadata + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @param null|SegmentInterface $segment + */ + public function execute(FileInterface $file, MetadataInterface $metadata, ?SegmentInterface $segment): FileInterface + { + if (!is_callable('iptcembed') && !is_callable('iptcparse')) { + throw new LocalizedException(__('iptcembed() && iptcparse() must be enabled in php configuration')); + } + + $iptcData = $segment ? iptcparse($segment->getData()) : []; + + if ($metadata->getTitle() !== null) { + $iptcData[self::IPTC_TITLE_SEGMENT][0] = $metadata->getTitle(); + } + + if ($metadata->getDescription() !== null) { + $iptcData[self::IPTC_DESCRIPTION_SEGMENT][0] = $metadata->getDescription(); + } + + if ($metadata->getKeywords() !== null) { + $iptcData = $this->writeKeywords($metadata->getKeywords(), $iptcData); + } + + $newData = ''; + + foreach ($iptcData as $tag => $values) { + foreach ($values as $value) { + $newData .= $this->iptcMaketag(2, (int) substr($tag, 2), $value); + } + } + + $this->writeFile($file->getPath(), iptcembed($newData, $file->getPath())); + + $fileWithIptc = $this->fileReader->execute($file->getPath()); + + return $this->fileFactory->create([ + 'path' => $fileWithIptc->getPath(), + 'segments' => $this->getSegmentsWithIptc($fileWithIptc, $file) + ]); + } + + /** + * Return iptc segment from file. + * + * @param FileInterface $fileWithIptc + * @param FileInterface $originFile + */ + private function getSegmentsWithIptc(FileInterface $fileWithIptc, $originFile): array + { + $segments = $fileWithIptc->getSegments(); + $originFileSegments = $originFile->getSegments(); + + foreach ($segments as $key => $segment) { + if ($segment->getName() === 'APP13') { + foreach ($originFileSegments as $originKey => $segment) { + if ($segment->getName() === 'APP13') { + $originFileSegments[$originKey] = $segments[$key]; + } + } + return $originFileSegments; + } + } + return $originFileSegments; + } + + /** + * Write keywords field to the iptc segment. + * + * @param array $keywords + * @param array $iptcData + */ + private function writeKeywords(array $keywords, array $iptcData): array + { + foreach ($keywords as $key => $keyword) { + $iptcData[self::IPTC_KEYWORDS_SEGMENT][$key] = $keyword; + } + return $iptcData; + } + + /** + * Write iptc data to the image directly to the file. + * + * @param string $filePath + * @param string $content + */ + private function writeFile(string $filePath, string $content): void + { + $resource = $this->driver->fileOpen($filePath, 'wb'); + + $this->driver->fileWrite($resource, $content); + $this->driver->fileClose($resource); + } + + /** + * Create new iptc tag text + * + * @param int $rec + * @param int $tag + * @param string $value + */ + private function iptcMaketag(int $rec, int $tag, string $value) + { + //phpcs:disable Magento2.Functions.DiscouragedFunction + $length = strlen($value); + $retval = chr(0x1C) . chr($rec) . chr($tag); + + if ($length < 0x8000) { + $retval .= chr($length >> 8) . chr($length & 0xFF); + } else { + $retval .= chr(0x80) . + chr(0x04) . + chr(($length >> 24) & 0xFF) . + chr(($length >> 16) & 0xFF) . + chr(($length >> 8) & 0xFF) . + chr($length & 0xFF); + } + //phpcs:enable Magento2.Functions.DiscouragedFunction + return $retval . $value; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php new file mode 100644 index 0000000000000..269df146f2c81 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to the XMP template + */ +class AddXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag'; + private const XMP_XPATH_SELECTOR_KEYWORDS_EACH = '//dc:subject/rdf:Bag/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORD_ITEM = 'rdf:li'; + + /** + * Parse metadata + * + * @param string $data + * @param MetadataInterface $metadata + * @return string + */ + public function execute(string $data, MetadataInterface $metadata): string + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + if ($metadata->getTitle() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE, $metadata->getTitle()); + } + if ($metadata->getDescription() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION, $metadata->getDescription()); + } + if ($metadata->getKeywords() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_KEYWORDS); + } else { + $this->updateKeywords($xml, $metadata->getKeywords()); + } + + $data = $xml->asXML(); + return str_replace("<?xml version=\"1.0\"?>\n", '', $data); + } + + /** + * Update keywords + * + * @param \SimpleXMLElement $xml + * @param array $keywords + */ + private function updateKeywords(\SimpleXMLElement $xml, array $keywords): void + { + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS_EACH) as $keywordElement) { + unset($keywordElement[0]); + } + + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) as $element) { + foreach ($keywords as $keyword) { + $element->addChild(self::XMP_XPATH_SELECTOR_KEYWORD_ITEM, $keyword); + } + } + } + + /** + * Deletes xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + */ + private function deleteValueByXpath(\SimpleXMLElement $xml, string $xpath): void + { + foreach ($xml->xpath($xpath) as $element) { + unset($element[0]); + } + } + + /** + * Set value to xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + * @param string $value + */ + private function setValueByXpath(\SimpleXMLElement $xml, string $xpath, string $value): void + { + foreach ($xml->xpath($xpath) as $element) { + $element[0] = $value; + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File.php b/app/code/Magento/MediaGalleryMetadata/Model/File.php new file mode 100644 index 0000000000000..4b7605e8ec839 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; + +/** + * File internal data transfer object + */ +class File implements FileInterface +{ + /** + * @var string + */ + private $path; + + /** + * @var array + */ + private $segments; + + /** + * @var FileExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param string $path + * @param array $segments + * @param FileExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $path, + array $segments, + ?FileExtensionInterface $extensionAttributes = null + ) { + $this->path = $path; + $this->segments = $segments; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * @inheritdoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?FileExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php new file mode 100644 index 0000000000000..d5918781135a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Add metadata to the asset by path. Should be used as a virtual type with a file type specific configuration + */ +class AddMetadata implements AddMetadataInterface +{ + /** + * @var array + */ + private $segmentWriters; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var WriteFileInterface + */ + private $fileWriter; + + /** + * @param FileInterfaceFactory $fileFactory + * @param ReadFileInterface $fileReader + * @param WriteFileInterface $fileWriter + * @param array $segmentWriters + */ + public function __construct( + FileInterfaceFactory $fileFactory, + ReadFileInterface $fileReader, + WriteFileInterface $fileWriter, + array $segmentWriters + ) { + $this->fileFactory = $fileFactory; + $this->fileReader = $fileReader; + $this->fileWriter = $fileWriter; + $this->segmentWriters = $segmentWriters; + } + + /** + * @inheritdoc + */ + public function execute(string $path, MetadataInterface $metadata): void + { + try { + $file = $this->fileReader->execute($path); + } catch (ValidatorException $e) { + return; + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not parse the image file for metadata: %path', ['path' => $path]) + ); + } + + try { + $this->fileWriter->execute($this->writeMetadata($file, $metadata)); + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not update the image file metadata: %path', ['path' => $path]) + ); + } + } + + /** + * Write metadata by given metadata writer + * + * @param FileInterface $file + * @param MetadataInterface $metadata + */ + private function writeMetadata(FileInterface $file, MetadataInterface $metadata): FileInterface + { + foreach ($this->segmentWriters as $writer) { + if (!$writer instanceof WriteMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . WriteFileInterface::class) + ); + } + + $file = $writer->execute($file, $metadata); + } + return $file; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php new file mode 100644 index 0000000000000..d9a8202281fff --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; + +/** + * Extract Metadata from asset file by given extractors + */ +class ExtractMetadata implements ExtractMetadataInterface +{ + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var array + */ + private $segmentReaders; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param MetadataInterfaceFactory $metadataFactory + * @param ReadFileInterface $fileReader + * @param array $segmentReaders + */ + public function __construct( + FileInterfaceFactory $fileFactory, + MetadataInterfaceFactory $metadataFactory, + ReadFileInterface $fileReader, + array $segmentReaders + ) { + $this->fileFactory = $fileFactory; + $this->metadataFactory = $metadataFactory; + $this->fileReader = $fileReader; + $this->segmentReaders = $segmentReaders; + } + + /** + * @inheritdoc + */ + public function execute(string $path): MetadataInterface + { + try { + return $this->extractMetadata($path); + } catch (\Exception $exception) { + return $this->metadataFactory->create(); + } + } + + /** + * Extract metadata from file + * + * @param string $path + * @return MetadataInterface + */ + private function extractMetadata(string $path): MetadataInterface + { + try { + $file = $this->fileReader->execute($path); + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not parse the image file for metadata: %path', ['path' => $path]) + ); + } + + return $this->readSegments($file); + } + + /** + * Read file segments by given segmentReader + * + * @param FileInterface $file + */ + private function readSegments(FileInterface $file): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->segmentReaders as $segmentReader) { + if (!$segmentReader instanceof ReadMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($segmentReader) . ' must implement ' . ReadMetadataInterface::class) + ); + } + + $data = $segmentReader->execute($file); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php new file mode 100644 index 0000000000000..d7290f31ee34e --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Get metadata from IPTC block + */ +class GetIptcMetadata +{ + private const IPTC_TITLE = '2#005'; + private const IPTC_DESCRIPTION = '2#120'; + private const IPTC_KEYWORDS = '2#025'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $title = ''; + $description = ''; + $keywords = []; + + if (is_callable('iptcparse')) { + $iptcData = iptcparse($data); + + if (!empty($iptcData[self::IPTC_TITLE])) { + $title = trim($iptcData[self::IPTC_TITLE][0]); + } + + if (!empty($iptcData[self::IPTC_DESCRIPTION][0])) { + $description = trim($iptcData[self::IPTC_DESCRIPTION][0]); + } + + if (!empty($iptcData[self::IPTC_KEYWORDS][0])) { + $keywords = array_values($iptcData[self::IPTC_KEYWORDS]); + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php new file mode 100644 index 0000000000000..bda01645ddfec --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; + +/** + * Get metadata from XMP block + */ +class GetXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag/rdf:li'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct(MetadataInterfaceFactory $metadataFactory) + { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + $keywords = array_map( + function (\SimpleXMLElement $element): string { + return (string) $element; + }, + $xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) + ); + + $description = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_DESCRIPTION)); + $title = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_TITLE)); + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php new file mode 100644 index 0000000000000..88810d3ccf28f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php @@ -0,0 +1,318 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + + $header = $this->read($resource, 3); + + if ($header != "GIF") { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a GIF image')); + } + + $version = $this->read($resource, 3); + + if (!in_array($version, ['87a', '89a'])) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('Unexpected GIF version')); + } + + $headerSegment = $this->segmentFactory->create([ + 'name' => 'header', + 'data' => $header . $version + ]); + + $width = $this->read($resource, 2); + $height = $this->read($resource, 2); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $backgroundAndAspectRatio = $this->read($resource, 2); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + + $generalSegment = $this->segmentFactory->create([ + 'name' => 'header2', + 'data' => $width . $height . $bitPerPixelBinary . $backgroundAndAspectRatio . $globalColorTable + ]); + + $segments = $this->getSegments($resource); + + array_unshift($segments, $headerSegment, $generalSegment); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read gif segments + * + * @param resource $resource + * @return SegmentInterface[] + * @throws FileSystemException + */ + private function getSegments($resource): array + { + $gifFrameSeparator = pack("C", ord(",")); + $gifExtensionSeparator = pack("C", ord("!")); + $gifTerminator = pack("C", ord(";")); + + $segments = []; + do { + $separator = $this->read($resource, 1); + + if ($separator == $gifTerminator) { + return $segments; + } + + if ($separator == $gifFrameSeparator) { + $segments[] = $this->segmentFactory->create([ + 'name' => 'frame', + 'data' => $gifFrameSeparator . $this->readFrame($resource) + ]); + continue; + } + + if ($separator != $gifExtensionSeparator) { + throw new LocalizedException(__('The file is corrupted')); + } + + $segments[] = $this->getExtensionSegment($resource); + } while (!$this->driver->endOfFile($resource)); + + return $segments; + } + + /** + * Read extension segment + * + * @param resource $resource + * @return SegmentInterface + * @throws FileSystemException + */ + private function getExtensionSegment($resource): SegmentInterface + { + $gifExtensionSeparator = pack("C", ord("!")); + $extensionCodeBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $extensionCode = unpack('C', $extensionCodeBinary)[1]; + + if ($extensionCode == 0xF9) { + return $this->segmentFactory->create([ + 'name' => 'Graphics Control Extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode == 0xFE) { + return $this->segmentFactory->create([ + 'name' => 'comment', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode != 0xFF) { + return $this->segmentFactory->create([ + 'name' => 'Programm extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + $blockLengthBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $blockLength = unpack('C', $blockLengthBinary)[1]; + $name = $this->read($resource, $blockLength); + + if ($blockLength != 11) { + throw new LocalizedException(__('The file is corrupted')); + } + + if ($name == 'XMP DataXMP') { + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlockWithSubblocks($resource) + ]); + } + + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlock($resource) + ]); + } + + /** + * Read gif frame + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readFrame($resource): string + { + $boundingBox = $this->read($resource, 8); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + return $boundingBox . $bitPerPixelBinary . $globalColorTable . $this->read($resource, 1) + . $this->readBlockWithSubblocks($resource); + } + + /** + * Retrieve bits per pixel value + * + * @param string $data + * @return int + */ + private function getBitPerPixel(string $data): int + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $bitPerPixel = unpack('C', $data)[1]; + $bpp = ($bitPerPixel & 7) + 1; + $bitPerPixel >>= 7; + $haveMap = $bitPerPixel & 1; + return $haveMap ? $bpp : 0; + } + + /** + * Read global color table + * + * @param resource $resource + * @param int $bitPerPixel + * @return string + * @throws FileSystemException + */ + private function getGlobalColorTable($resource, int $bitPerPixel): string + { + $globalColorTable = ''; + if ($bitPerPixel > 0) { + $max = pow(2, $bitPerPixel); + for ($i = 1; $i <= $max; ++$i) { + $globalColorTable .= $this->read($resource, 3); + } + } + return $globalColorTable; + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } + + /** + * Read the block stored in multiple sections + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readBlockWithSubblocks($resource): string + { + $data = ''; + $subLength = $this->read($resource, 1); + + while ($subLength !== "\0") { + $data .= $subLength . $this->read($resource, ord($subLength)); + $subLength = $this->read($resource, 1); + } + + return $data . $subLength; + } + + /** + * Read gif block + * + * @param resource $resource + * @return string + * @throws FileSystemException] + */ + private function readBlock($resource): string + { + $blockLengthBinary = $this->read($resource, 1); + $blockLength = ord($blockLengthBinary); + if ($blockLength == 0) { + return ''; + } + return $blockLengthBinary . $this->read($resource, $blockLength) . $this->read($resource, 1); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php new file mode 100644 index 0000000000000..1b83554ef4df3 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * XMP Reader for gif file format + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + /** + * see XMP Specification Part 3, 1.1.2 GIF + */ + private const MAGIC_TRAILER_LENGTH = 258; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + $xmp = substr($segment->getData(), 14); + + if (substr($xmp, -self::MAGIC_TRAILER_LENGTH, 3) !== self::MAGIC_TRAILER_START + || substr($xmp, -5) !== self::MAGIC_TRAILER_END + ) { + throw new LocalizedException(__('XMP data is corrupted')); + } + + return substr($xmp, 0, -self::MAGIC_TRAILER_LENGTH); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php new file mode 100644 index 0000000000000..2b5167eba596b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php @@ -0,0 +1,191 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for GIF format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + private const XMP_DATA_START_POSITION = 14; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $gifSegments = $file->getSegments(); + $xmpGifSegments = []; + foreach ($gifSegments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpGifSegments[$key] = $segment; + } + } + + if (empty($xmpGifSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpGifSegment($gifSegments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpGifSegments as $key => $segment) { + $gifSegments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $gifSegments + ]); + } + + /** + * Insert XMP segment to gif image segments (at position 3) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpGifSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 4), [$xmpSegment], array_slice($segments, 4)); + } + + /** + * Return XMP template from string + * + * @param string $string + * @param string $start + * @param string $end + */ + private function getXmpData(string $string, string $start, string $end): string + { + $string = ' ' . $string; + $ini = strpos($string, $start); + if ($ini == 0) { + return ''; + } + $ini += strlen($start); + $len = strpos($string, $end, $ini) - $ini; + + return substr($string, $ini, $len); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + + $xmpSegment = pack("C", ord("!")) . pack("C", 255) . pack("C", 11) . + self::XMP_SEGMENT_NAME . $this->addXmpMetadata->execute($xmpData, $metadata) . "\x01"; + + /** + * Write Magic trailer 258 bytes see XMP Specification Part 3, 1.1.2 GIF + */ + $i = 255; + while ($i > 0) { + $xmpSegment .= pack("C", $i); + $i--; + } + + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => $xmpSegment . "\0\0" + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = $this->getXmpData($data, self::XMP_SEGMENT_NAME, "\x01"); + $end = substr($data, strpos($data, "\x01")); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) . $end + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php new file mode 100644 index 0000000000000..cbdc9fa286e85 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments writer + */ +class WriteFile implements WriteFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write gif segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite( + $resource, + $segment->getData() + ); + } + $this->driver->fileWrite($resource, pack("C", ord(";"))); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php new file mode 100644 index 0000000000000..4dbff068a861b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; + +/** + * Jpeg file reader + */ +class ReadFile implements ReadFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + private const MARKER_IMAGE_START = "\xDA"; + + private const TWO_BYTES = 2; + private const ONE_MEGABYTE = 1048576; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * Is reader applicable + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + public function isApplicable(string $path): bool + { + $resource = $this->driver->fileOpen($path, 'rb'); + try { + $marker = $this->readMarker($resource); + } catch (LocalizedException $exception) { + return false; + } + $this->driver->fileClose($resource); + + return $marker == self::MARKER_IMAGE_FILE_START; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + if (!$this->isApplicable($path)) { + throw new ValidatorException(__('Not a JPEG image')); + } + + $resource = $this->driver->fileOpen($path, 'rb'); + $marker = $this->readMarker($resource); + + if ($marker != self::MARKER_IMAGE_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a JPEG image')); + } + + do { + $marker = $this->readMarker($resource); + $segments[] = $this->readSegment($resource, ord($marker)); + } while (($marker != self::MARKER_IMAGE_START) && (!$this->driver->endOfFile($resource))); + + if ($marker != self::MARKER_IMAGE_START) { + throw new LocalizedException(__('File is corrupted')); + } + + $segments[] = $this->segmentFactory->create([ + 'name' => 'CompressedImage', + 'data' => $this->readCompressedImage($resource) + ]); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read jpeg marker + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readMarker($resource): string + { + $data = $this->read($resource, self::TWO_BYTES); + + if ($data[0] != self::MARKER_PREFIX) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('File is corrupted')); + } + + return $data[1]; + } + + /** + * Read compressed image + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readCompressedImage($resource): string + { + $compressedImage = ''; + do { + $compressedImage .= $this->read($resource, self::ONE_MEGABYTE); + } while (!$this->driver->endOfFile($resource)); + + $endOfImageMarkerPosition = strpos($compressedImage, self::MARKER_PREFIX . self::MARKER_IMAGE_END); + + if ($endOfImageMarkerPosition !== false) { + $compressedImage = substr($compressedImage, 0, $endOfImageMarkerPosition); + } + + return $compressedImage; + } + + /** + * Read jpeg segment + * + * @param resource $resource + * @param int $segmentType + * @return SegmentInterface + * @throws FileSystemException + */ + private function readSegment($resource, int $segmentType): SegmentInterface + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentSize = unpack('nsize', $this->read($resource, 2))['size'] - 2; + return $this->segmentFactory->create([ + 'name' => $this->segmentNames->getSegmentName($segmentType), + 'data' => $this->read($resource, $segmentSize) + ]); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php new file mode 100644 index 0000000000000..94ccb400e5e0a --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetIptcMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for jpeg image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetIptcMetadata + */ + private $getIptcData; + + /** + * @param GetIptcMetadata $getIptcData + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + GetIptcMetadata $getIptcData, + MetadataInterfaceFactory $metadataFactory + ) { + $this->getIptcData = $getIptcData; + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + return $this->getIptcData->execute($segment->getData()); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php new file mode 100644 index 0000000000000..81ff7200c3475 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Jpeg XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isSegmentXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), self::XMP_DATA_START_POSITION); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php new file mode 100644 index 0000000000000..e9fcd500f1dca --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddIptcMetadata; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg IPTC Writer + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0\0x00'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddIPtcMetadata + */ + private $addIptcMetadata; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddIptcMetadata $addIptcMetadata + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddIptcMetadata $addIptcMetadata, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addIptcMetadata = $addIptcMetadata; + $this->fileReader = $fileReader; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $iptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $iptcSegments[$key] = $segment; + } + } + + foreach ($iptcSegments as $segment) { + return $this->addIptcMetadata->execute($file, $metadata, $segment); + } + return $this->addIptcMetadata->execute($file, $metadata, null); + } + + /** + * Check if segment contains IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php new file mode 100644 index 0000000000000..e88cdd5b7b8f4 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg XMP Writer + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $xmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpSegments[$key] = $segment; + } + } + + if (empty($xmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpSegment($segments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = substr($data, self::XMP_DATA_START_POSITION); + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php new file mode 100644 index 0000000000000..403bc7f3d7449 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_IMAGE_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + foreach ($file->getSegments() as $segment) { + if ($segment->getName() != 'CompressedImage' && strlen($segment->getData()) > 0xfffd) { + throw new LocalizedException(__('A Header is too large to fit in the segment!')); + } + } + + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_END); + $this->driver->fileClose($resource); + } + + /** + * Write jpeg segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + if ($segment->getName() !== 'CompressedImage') { + $this->driver->fileWrite( + $resource, + //phpcs:ignore Magento2.Functions.DiscouragedFunction + self::MARKER_IMAGE_PREFIX . chr($this->segmentNames->getSegmentType($segment->getName())) + ); + $this->driver->fileWrite($resource, pack("n", strlen($segment->getData()) + 2)); + } + $this->driver->fileWrite($resource, $segment->getData()); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php new file mode 100644 index 0000000000000..9e3ee5d29a495 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Media asset metadata data transfer object + */ +class Metadata implements MetadataInterface +{ + /** + * @var string + */ + private $title; + + /** + * @var string + */ + private $description; + + /** + * @var array + */ + private $keywords; + + /** + * @var MetadataExtensionInterface + */ + private $extensionAttributes; + + /** + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @param MetadataExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $title = null, + string $description = null, + array $keywords = null, + ?MetadataExtensionInterface $extensionAttributes = null + ) { + $this->title = $title; + $this->description = $description; + $this->keywords = $keywords; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getTitle(): ?string + { + return $this->title; + } + + /** + * @inheritdoc + */ + public function getKeywords(): ?array + { + return $this->keywords; + } + + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php new file mode 100644 index 0000000000000..673f8ff436ebe --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + private const PNG_MARKER_IMAGE_END = 'IEND'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + $header = $this->readHeader($resource); + + if ($header != self::PNG_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a PNG image')); + } + + do { + $header = $this->readHeader($resource); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentHeader = unpack('Nsize/a4type', $header); + $data = $this->read($resource, $segmentHeader['size']); + $segments[] = $this->segmentFactory->create([ + 'name' => $segmentHeader['type'], + 'data' => $data + ]); + $cyclicRedundancyCheck = $this->read($resource, 4); + + if (pack('N', crc32($segmentHeader['type'] . $data)) != $cyclicRedundancyCheck) { + throw new LocalizedException(__('The image is corrupted')); + } + } while ($header + && $segmentHeader['type'] != self::PNG_MARKER_IMAGE_END + && !$this->driver->endOfFile($resource) + ); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read 8 bytes + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readHeader($resource): string + { + return $this->read($resource, 8); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php new file mode 100644 index 0000000000000..c856d95475a40 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for png image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_CHUNK_MARKER_LENGTH = 4; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + return $this->getIptcData($segment); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Read iptc data from zTXt segment + * + * @param SegmentInterface $segment + */ + private function getIptcData(SegmentInterface $segment): MetadataInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker); + if ($descriptionStartPosition) { + $description = substr( + $binData, + $descriptionStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $descriptionStartPosition + 3, 1)) + ); + } + + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker); + if ($titleStartPosition) { + $title = substr( + $binData, + $titleStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $titleStartPosition + 3, 1)) + ); + } + + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker); + if ($keywordsStartPosition) { + $keywords = substr( + $binData, + $keywordsStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $keywordsStartPosition + 3, 1)) + ); + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? explode(',', $keywords) : null + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php new file mode 100644 index 0000000000000..83ba554f7bf5d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * PNG XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmpSegment($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php new file mode 100644 index 0000000000000..d40dbc13d2962 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * IPTC Writer to write IPTC data for png image + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_SEGMENT_START_STRING = 'Raw profile type iptc'; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * Write iptc metadata to zTXt segment + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngIptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $pngIptcSegments[$key] = $segment; + } + } + + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + + if (empty($pngIptcSegments)) { + $segments[] = $this->createPngIptcSegment($metadata); + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + foreach ($pngIptcSegments as $key => $segment) { + $segments[$key] = $this->updateIptcSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Create new zTXt segment with metadata + * + * @param MetadataInterface $metadata + */ + private function createPngIptcSegment(MetadataInterface $metadata): SegmentInterface + { + $start = '8BIM' . str_repeat(pack('C', 4), 2) . str_repeat(pack("C", 0), 5) + . 'c' . pack('C', 28) . pack('C', 1); + $compression = 'Z' . pack('C', 0) . pack('C', 3) . pack('C', 27) . '%G' . pack('C', 28) . pack('C', 1); + $end = str_repeat(pack('C', 0), 2) . pack('C', 2) . pack('C', 0) . pack('C', 4) . pack('C', 28); + $binData = $start . $compression . $end; + + $description = $metadata->getDescription(); + if ($description !== null) { + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $binData .= $descriptionMarker . pack('C', strlen($description)) . $description . pack('C', 28); + } + + $title = $metadata->getTitle(); + if ($title !== null) { + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $binData .= $titleMarker . pack('C', strlen($title)) . $title . pack('C', 28); + } + + $keywords = $metadata->getKeywords(); + if ($keywords !== null) { + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywords = implode(',', $keywords); + $binData .= $keywordsMarker . pack('C', strlen($keywords)) . $keywords . pack('C', 28); + } + + $binData .= pack('C', 0); + $hexString = bin2hex($binData); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $compressedIptcData = gzcompress(PHP_EOL . 'iptc' . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => self::IPTC_SEGMENT_NAME, + 'data' => self::IPTC_SEGMENT_START_STRING . str_repeat(pack('C', 0), 2) . $compressedIptcData + ]); + } + + /** + * Update iptc data to zTXt segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + */ + private function updateIptcSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + if ($metadata->getDescription() !== null) { + $description = $metadata->getDescription(); + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($description)) . $description, + $descriptionStartPosition + ) . substr($binData, $descriptionStartPosition + 1 + ord(substr($binData, $descriptionStartPosition))); + } + + if ($metadata->getTitle() !== null) { + $title = $metadata->getTitle(); + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($title)) . $title, + $titleStartPosition + ) . substr($binData, $titleStartPosition + 1 + ord(substr($binData, $titleStartPosition))); + } + + if ($metadata->getKeywords() !== null) { + $keywords = implode(',', $metadata->getKeywords()); + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($keywords)) . $keywords, + $keywordsStartPosition + ) . substr($binData, $keywordsStartPosition + 1 + ord(substr($binData, $keywordsStartPosition))); + } + $hexString = bin2hex($binData); + $iptcSegmentStart = substr($segment->getData(), 0, $iptSegmentStartPosition + 2); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentDataCompressed = gzcompress(PHP_EOL . $data[0] . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $iptcSegmentStart . $segmentDataCompressed + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php new file mode 100644 index 0000000000000..9d8d5d975d99d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for png format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + private const XMP_SEGMENT_START = "XML:com.adobe.xmp\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add xmp metadata to the png file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngXmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isXmpSegment($segment)) { + $pngXmpSegments[$key] = $segment; + } + } + + if (empty($pngXmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertPngXmpSegment($segments, $this->createPngXmpSegment($metadata)) + ]); + } + + foreach ($pngXmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image png segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertPngXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new png segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function createPngXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the png xmp segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($this->getXmpData($segment), $metadata) + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php new file mode 100644 index 0000000000000..c5db6644b3545 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write PNG file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::PNG_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write PNG segments + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite($resource, pack("N", strlen($segment->getData()))); + $this->driver->fileWrite($resource, pack("a4", $segment->getName())); + $this->driver->fileWrite($resource, $segment->getData()); + $this->driver->fileWrite($resource, pack("N", crc32($segment->getName() . $segment->getData()))); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Segment.php b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php new file mode 100644 index 0000000000000..0e8a89767e40c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Segment internal data transfer object + */ +class Segment implements SegmentInterface +{ + /** + * @var array + */ + private $name; + + /** + * @var string + */ + private $data; + + /** + * @var SegmentExtensionInterface + */ + private $extensionAttributes; + + /** + * @param string $name + * @param string $data + * @param SegmentExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $name, + string $data, + ?SegmentExtensionInterface $extensionAttributes = null + ) { + $this->name = $name; + $this->data = $data; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritdoc + */ + public function getData(): string + { + return $this->data; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php new file mode 100644 index 0000000000000..62eea09453ae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +/** + * Segment types to names mapper + */ +class SegmentNames +{ + private const SEGMENT_TYPE_TO_NAME = [ + 0xC0 => "SOF0", + 0xC1 => "SOF1", + 0xC2 => "SOF2", + 0xC3 => "SOF4", + 0xC5 => "SOF5", + 0xC6 => "SOF6", + 0xC7 => "SOF7", + 0xC8 => "JPG", + 0xC9 => "SOF9", + 0xCA => "SOF10", + 0xCB => "SOF11", + 0xCD => "SOF13", + 0xCE => "SOF14", + 0xCF => "SOF15", + 0xC4 => "DHT", + 0xCC => "DAC", + 0xD0 => "RST0", + 0xD1 => "RST1", + 0xD2 => "RST2", + 0xD3 => "RST3", + 0xD4 => "RST4", + 0xD5 => "RST5", + 0xD6 => "RST6", + 0xD7 => "RST7", + 0xD8 => "SOI", + 0xD9 => "EOI", + 0xDA => "SOS", + 0xDB => "DQT", + 0xDC => "DNL", + 0xDD => "DRI", + 0xDE => "DHP", + 0xDF => "EXP", + 0xE0 => "APP0", + 0xE1 => "APP1", + 0xE2 => "APP2", + 0xE3 => "APP3", + 0xE4 => "APP4", + 0xE5 => "APP5", + 0xE6 => "APP6", + 0xE7 => "APP7", + 0xE8 => "APP8", + 0xE9 => "APP9", + 0xEA => "APP10", + 0xEB => "APP11", + 0xEC => "APP12", + 0xED => "APP13", + 0xEE => "APP14", + 0xEF => "APP15", + 0xF0 => "JPG0", + 0xF1 => "JPG1", + 0xF2 => "JPG2", + 0xF3 => "JPG3", + 0xF4 => "JPG4", + 0xF5 => "JPG5", + 0xF6 => "JPG6", + 0xF7 => "JPG7", + 0xF8 => "JPG8", + 0xF9 => "JPG9", + 0xFA => "JPG10", + 0xFB => "JPG11", + 0xFC => "JPG12", + 0xFD => "JPG13", + 0xFE => "COM", + 0x01 => "TEM", + 0x02 => "RES", + ]; + + /** + * Get segment name by type + * + * @param int $type + * @return string + */ + public function getSegmentName(int $type): string + { + return self::SEGMENT_TYPE_TO_NAME[$type]; + } + + /** + * Get segment type by name + * + * @param string $name + * @return int + */ + public function getSegmentType(string $name): int + { + return array_search($name, self::SEGMENT_TYPE_TO_NAME); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php new file mode 100644 index 0000000000000..a7d07f66ba8aa --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Module\Dir; +use Magento\Framework\Module\Dir\Reader; + +/** + * XMP template provider + */ +class XmpTemplate +{ + private const XMP_TEMPLATE_FILENAME = 'default.xmp'; + + /** + * @var Reader + */ + private $moduleReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @param Reader $moduleReader + * @param DriverInterface $driver + */ + public function __construct(Reader $moduleReader, DriverInterface $driver) + { + $this->moduleReader = $moduleReader; + $this->driver = $driver; + } + + /** + * Get default XMP template + * + * @return string + * @throws FileSystemException + */ + public function get(): string + { + $etcDirectoryPath = $this->moduleReader->getModuleDir( + Dir::MODULE_ETC_DIR, + 'Magento_MediaGalleryMetadata' + ); + return $this->driver->fileGetContents( + $etcDirectoryPath . '/' . self::XMP_TEMPLATE_FILENAME + ); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/README.md b/app/code/Magento/MediaGalleryMetadata/README.md new file mode 100644 index 0000000000000..ec74e527ddebb --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadata + +The purpose of this module is to provide an ability to extract the metadata from file and populating Media Asset entity fields when an image is uploaded to Magento and also provide an ability to update the metadata stored in an image file. diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php new file mode 100644 index 0000000000000..c284bf71e60af --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php @@ -0,0 +1,197 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * ExtractMetadata test + */ +class AddMetadataTest extends TestCase +{ + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->addMetadata = Bootstrap::getObjectManager()->get(AddMetadataInterface::class); + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataInterfaceFactory::class); + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param null|string $fileName + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws LocalizedException + */ + public function testExecute( + ?string $fileName, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $metadata = $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + + $this->addMetadata->execute($modifiableFilePath, $metadata); + + $updatedMetadata = $this->extractMetadata->execute($modifiableFilePath); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'iptc_only.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + null, + null + ], + [ + 'iptc_only.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_iptc.jpeg', + 'Updated Title', + null, + null + ], + [ + 'macos-preview.png', + 'Title of the magento image 2', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ], + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'empty_xmp_image.png', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php new file mode 100644 index 0000000000000..982ccbb20fe2c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for ExtractMetadata + */ +class ExtractMetadataTest extends TestCase +{ + /** + * @var ExtractMetadataComposite + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testExecute( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $metadata = $this->extractMetadata->execute($path); + + $this->assertEquals($title, $metadata->getTitle()); + $this->assertEquals($description, $metadata->getDescription()); + $this->assertEquals($keywords, $metadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'macos-photos.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'macos-preview.png', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'exiftool.gif', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.png', + 'Title of the magento image', + 'PNG format is awesome', + [ + 'png', + 'awesome' + ] + ], + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php new file mode 100644 index 0000000000000..4bba73e3ca2a9 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Gif\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer gif format + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteReadGif( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalGifMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalGifMetadata->getTitle()); + $this->assertEmpty($originalGifMetadata->getDescription()); + $this->assertEmpty($originalGifMetadata->getKeywords()); + $updatedGifFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedGifMetadata = $this->xmpReader->execute($updatedGifFile); + $this->assertEquals($title, $updatedGifMetadata->getTitle()); + $this->assertEquals($description, $updatedGifMetadata->getDescription()); + $this->assertEquals($keywords, $updatedGifMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_exiftool.gif', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php new file mode 100644 index 0000000000000..932b71df28430 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php new file mode 100644 index 0000000000000..043e26f67853f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + $updatedFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedMetadata = $this->xmpReader->execute($updatedFile); + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php new file mode 100644 index 0000000000000..d8bcfd7a94561 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Png\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Png\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif new file mode 100644 index 0000000000000..14cc6026b5950 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg new file mode 100644 index 0000000000000..144a56dac2d3e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png new file mode 100644 index 0000000000000..129c49a1b7e64 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg new file mode 100644 index 0000000000000..cee7bff38a6c6 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png new file mode 100644 index 0000000000000..7e81891ebc0ee Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif new file mode 100644 index 0000000000000..70574d70b609e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg new file mode 100644 index 0000000000000..5d7dba35fede7 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png new file mode 100644 index 0000000000000..9b4821c1c4e5d Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg new file mode 100644 index 0000000000000..3a07b6abe788e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png new file mode 100644 index 0000000000000..966520f0d0112 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/composer.json b/app/code/Magento/MediaGalleryMetadata/composer.json new file mode 100644 index 0000000000000..c2ce66ce64c36 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-media-gallery-metadata", + "description": "Magento module responsible for images metadata processing", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-metadata-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadata\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/etc/default.xmp b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp new file mode 100644 index 0000000000000..772b6af671ec6 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp @@ -0,0 +1,24 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> +<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0"> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description rdf:about="" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"> + <dc:subject> + <rdf:Seq> + <rdf:li>magento</rdf:li> + </rdf:Seq> + </dc:subject> + <dc:description><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:description> + <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:title> + <dc:subject> + <rdf:Bag> + <rdf:li>magento</rdf:li> + <rdf:li>mediagallerymetadata</rdf:li> + </rdf:Bag> + </dc:subject> + </rdf:Description> + </rdf:RDF> +</x:xmpmeta> +<?xpacket end="w"?> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/etc/di.xml b/app/code/Magento/MediaGalleryMetadata/etc/di.xml new file mode 100644 index 0000000000000..d2f1f90510488 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/di.xml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface" type="Magento\MediaGalleryMetadata\Model\Metadata"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\FileInterface" type="Magento\MediaGalleryMetadata\Model\File"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\SegmentInterface" type="Magento\MediaGalleryMetadata\Model\Segment"/> + <type name="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"> + <arguments> + <argument name="writers" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\AddMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\AddMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\XmpTemplate"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\AddIptcMetadata"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/etc/module.xml b/app/code/Magento/MediaGalleryMetadata/etc/module.xml new file mode 100644 index 0000000000000..776b05aecd284 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryMetadata"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/registration.php b/app/code/Magento/MediaGalleryMetadata/registration.php new file mode 100644 index 0000000000000..fcf6789d9321f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryMetadata', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php new file mode 100644 index 0000000000000..df645681e8971 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to asset file + */ +interface AddMetadataInterface +{ + /** + * Add metadata to the asset file + * + * @param string $path + * @param MetadataInterface $metadata + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $metadata): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php new file mode 100644 index 0000000000000..63e943150f4a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; + +/** + * Media asset metadata data transfer object + */ +interface MetadataInterface extends ExtensibleDataInterface +{ + /** + * Get asset title + * + * @return null|string + */ + public function getTitle(): ?string; + + /** + * Get asset description + * + * @return null|string + */ + public function getDescription(): ?string; + + /** + * Get asset keywords + * + * @return null|array + */ + public function getKeywords(): ?array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php new file mode 100644 index 0000000000000..2327406db8bef --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Extract asset metadata + */ +interface ExtractMetadataInterface +{ + /** + * Extract metadata from the asset file + * + * @param string $path + * @return MetadataInterface + */ + public function execute(string $path): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php new file mode 100644 index 0000000000000..fc3f53313199d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer pool + */ +class AddMetadataComposite implements AddMetadataInterface +{ + /** + * @var AddMetadataInterface[] + */ + private $writers; + + /** + * @param AddMetadataInterface[] $writers + */ + public function __construct(array $writers) + { + $this->writers = $writers; + } + + /** + * Write metadata to the path + * + * @param string $path + * @param MetadataInterface $data + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $data): void + { + foreach ($this->writers as $writer) { + if (!$writer instanceof AddMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . AddMetadataInterface::class) + ); + } + + $writer->execute($path, $data); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php new file mode 100644 index 0000000000000..0d6e8aa345178 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; + +/** + * Metadata extractor composite + */ +class ExtractMetadataComposite implements ExtractMetadataInterface +{ + /** + * @var ExtractMetadataInterface[] + */ + private $extractors; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param ExtractMetadataInterface[] $extractors + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory, + array $extractors + ) { + $this->metadataFactory = $metadataFactory; + $this->extractors = $extractors; + } + + /** + * Extract metadata from file + * + * @param string $path + * @return MetadataInterface + * @throws LocalizedException + */ + public function execute(string $path): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->extractors as $extractor) { + if (!$extractor instanceof ExtractMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($extractor) . ' must implement ' . ExtractMetadataInterface::class) + ); + } + + $data = $extractor->execute($path); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php new file mode 100644 index 0000000000000..0cd01bbf57c64 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; + +/** + * File internal data transfer object + */ +interface FileInterface extends ExtensibleDataInterface +{ + /** + * Get file path + * + * @return string + */ + public function getPath(): string; + + /** + * Get metadata sections + * + * @return SegmentInterface[] + */ + public function getSegments(): array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null + */ + public function getExtensionAttributes(): ?FileExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php new file mode 100644 index 0000000000000..e45a934f7b5ad --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +/** + * File reader + */ +interface ReadFileInterface +{ + /** + * Create file object from the file + * + * @param string $path + * @return FileInterface + */ + public function execute(string $path): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php new file mode 100644 index 0000000000000..b6d97118f848b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata reader + */ +interface ReadMetadataInterface +{ + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php new file mode 100644 index 0000000000000..bf6cdc30306f8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; + +/** + * Segment internal data transfer object + */ +interface SegmentInterface extends ExtensibleDataInterface +{ + /** + * Get segment name + * + * @return string + */ + public function getName(): string; + + /** + * Get segment data + * + * @return string + */ + public function getData(): string; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php new file mode 100644 index 0000000000000..fe7579989c40f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; + +/** + * File writer + */ +interface WriteFileInterface +{ + /** + * Write file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php new file mode 100644 index 0000000000000..943879ebaec86 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer + */ +interface WriteMetadataInterface +{ + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $data + */ + public function execute(FileInterface $file, MetadataInterface $data): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/README.md b/app/code/Magento/MediaGalleryMetadataApi/README.md new file mode 100644 index 0000000000000..82f86d2f61c6d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadataApi + +The Magento_MediaGalleryMetadataApi module is responsible for the media gallery metadata implementation API. diff --git a/app/code/Magento/MediaGalleryMetadataApi/composer.json b/app/code/Magento/MediaGalleryMetadataApi/composer.json new file mode 100644 index 0000000000000..f8673884b050c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-metadata-api", + "description": "Magento module responsible for media gallery metadata implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadataApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml b/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml new file mode 100644 index 0000000000000..77adbc6efff88 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryMetadataApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadataApi/registration.php b/app/code/Magento/MediaGalleryMetadataApi/registration.php new file mode 100644 index 0000000000000..90988681a5483 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryMetadataApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md new file mode 100644 index 0000000000000..df856e8003a84 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditions module + +The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json new file mode 100644 index 0000000000000..50b18752fc506 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions", + "description": "Magento module that implements height and width fields for for media gallery items.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditions\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..f23f94f186f68 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery_renditions" translate="label" type="text" sortOrder="1010" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Media Gallery Renditions</label> + <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Width</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Height</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml new file mode 100644 index 0000000000000..58c5aa1f11fd2 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery_renditions> + <width>1000</width> + <height>1000</height> + </media_gallery_renditions> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml new file mode 100644 index 0000000000000..792a9e128cc40 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryRenditions" /> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php new file mode 100644 index 0000000000000..275c06f752a63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditions', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php b/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php new file mode 100644 index 0000000000000..e558f23ab9608 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class responsible for providing access to Media Gallery Renditions system configuration. + */ +class Config +{ + /** + * Config path for Media Gallery Renditions Width + */ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + + /** + * Config path for Media Gallery Renditions Height + */ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get max width + * + * @return int + */ + public function getWidth(): int + { + return $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } + + /** + * Get max height + * + * @return int + */ + public function getHeight(): int + { + return $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md new file mode 100644 index 0000000000000..42478c0c9b520 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditionsApi module + +The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json new file mode 100644 index 0000000000000..6e3c559f001c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions-api", + "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditionsApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml new file mode 100644 index 0000000000000..64efa325ec791 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryRenditionsApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php new file mode 100644 index 0000000000000..bf057f2d2adbf --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditionsApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php new file mode 100644 index 0000000000000..b4b6713f47065 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Asset; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the asset options for multiselect filter + */ +class Search extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var SearchAssetsInterface + */ + private $searchAssets; + + /** + * @param SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Images + */ + private $images; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var Storage + */ + private $storage; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param FilterBuilder $filterBuilder + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param SearchAssetsInterface $searchAssets + * @param Context $context + * @param LoggerInterface $logger + * @param Images $images + * @param Storage $storage + */ + public function __construct( + FilterBuilder $filterBuilder, + SearchCriteriaBuilder $searchCriteriaBuilder, + FilterGroupBuilder $filterGroupBuilder, + SearchAssetsInterface $searchAssets, + Context $context, + LoggerInterface $logger, + Images $images, + Storage $storage + ) { + parent::__construct($context); + + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->logger = $logger; + $this->searchAssets = $searchAssets; + $this->images = $images; + $this->storage = $storage; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $searchKey = $this->getRequest()->getParam('searchKey'); + $limit = $this->getRequest()->getParam('limit'); + $pageNum = $this->getRequest()->getParam('page'); + $responseContent = []; + + if (!$searchKey) { + return $resultJson->setData([ + 'options' => [], + 'total' => 0 + ]); + } + + try { + $titleFilter = $this->filterBuilder->setField('title') + ->setConditionType('fulltext') + ->setValue($searchKey) + ->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$this->filterGroupBuilder->setFilters([$titleFilter])->create()]) + ->setPageSize($limit) + ->setCurrentPage($pageNum < 2 ? 0 : $pageNum) + ->create(); + + $assets = $this->searchAssets->execute($searchCriteria); + + if (!empty($assets)) { + foreach ($assets as $asset) { + $responseContent['options'][] = [ + 'value' => $asset->getId(), + 'label' => $asset->getTitle(), + 'path' => $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()) + ]; + $responseContent['total'] = count($responseContent['options']); + } + } + + $responseCode = self::HTTP_OK; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php new file mode 100644 index 0000000000000..3d4af88e4ad67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller to create the folders + */ +class Create extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var CreateDirectoriesByPathsInterface + */ + private $createDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param CreateDirectoriesByPathsInterface $createDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + CreateDirectoriesByPathsInterface $createDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->createDirectoriesByPaths = $createDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Create folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $paths = $this->getRequest()->getParam('paths'); + + if (!$paths) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder paths parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->createDirectoriesByPaths->execute($paths); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully created the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to create folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php new file mode 100644 index 0000000000000..56f12c5139d65 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the folders + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var DeleteDirectoriesByPathsInterface + */ + private $deleteDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->deleteDirectoriesByPaths = $deleteDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Delete folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $path = $this->getRequest()->getParam('path'); + + if (!$path) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder path parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->deleteDirectoriesByPaths->execute([$path]); + $this->deleteAssetsByPaths->execute([$path]); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully removed the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to remove folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php new file mode 100644 index 0000000000000..229a717ef13dd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\MediaGalleryUi\Model\Directories\FolderTree; +use Psr\Log\LoggerInterface; + +/** + * Returns all available directories + */ +class GetTree extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FolderTree + */ + private $folderTree; + + /** + * Constructor + * + * @param Action\Context $context + * @param LoggerInterface $logger + * @param FolderTree $folderTree + */ + public function __construct( + Action\Context $context, + LoggerInterface $logger, + FolderTree $folderTree + ) { + parent::__construct($context); + $this->logger = $logger; + $this->folderTree = $folderTree; + } + /** + * @inheritdoc + */ + public function execute() + { + try { + $responseContent[] = $this->folderTree->buildTree(); + $responseCode = self::HTTP_OK; + } catch (\Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('Retrieving directories list failed.'), + ]; + } + + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php new file mode 100644 index 0000000000000..a5d1cee7abf41 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryUi\Model\DeleteImage; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the media gallery content + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var DeleteImage + */ + private $deleteImage; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Delete constructor. + * + * @param Context $context + * @param DeleteImage $deleteImage + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param Storage $imagesStorage + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteImage $deleteImage, + GetAssetsByIdsInterface $getAssetsByIds, + Storage $imagesStorage, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteImage = $deleteImage; + $this->getAssetsByIds = $getAssetsByIds; + $this->imagesStorage = $imagesStorage; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $imageIds = $this->getRequest()->getParam('ids'); + + if (empty($imageIds) || !is_array($imageIds)) { + $responseContent = [ + 'success' => false, + 'message' => __('Image Ids are required and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $assets = $this->getAssetsByIds->execute($imageIds); + $this->deleteImage->execute($assets); + $responseCode = self::HTTP_OK; + if (count($imageIds) === 1) { + $message = __( + 'The asset "%title" has been successfully deleted.', + [ + 'title' => current($assets)->getTitle() + ] + ); + } else { + $message = __( + '%count assets have been successfully deleted.', + [ + 'count' => count($imageIds) + ] + ); + } + $responseContent = [ + 'success' => true, + 'message' => $message, + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to delete image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php new file mode 100644 index 0000000000000..d959a070148ed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\GetDetailsByAssetId; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the media gallery image details + */ +class Details extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var GetDetailsByAssetId + */ + private $getDetailsByAssetId; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Details constructor. + * + * @param Context $context + * @param GetDetailsByAssetId $getDetailsByAssetId + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + GetDetailsByAssetId $getDetailsByAssetId, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->logger = $logger; + $this->getDetailsByAssetId = $getDetailsByAssetId; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $ids = $this->getRequest()->getParam('ids'); + + if (empty($ids) || !is_array($ids)) { + $responseContent = [ + 'success' => false, + 'message' => __('Assets Ids is required, and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $details = $this->getDetailsByAssetId->execute($ids); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'imageDetails' => $details + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php new file mode 100644 index 0000000000000..f41c489607b15 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryUi\Model\UpdateAsset; +use Psr\Log\LoggerInterface; + +class SaveDetails extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var UpdateAsset + */ + private $updateAsset; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param MetadataInterfaceFactory $metadataFactory + * @param UpdateAsset $updateAsset + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + MetadataInterfaceFactory $metadataFactory, + UpdateAsset $updateAsset, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->metadataFactory = $metadataFactory; + $this->updateAsset = $updateAsset; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $assetId = (int) $this->getRequest()->getParam('id'); + $title = $this->getRequest()->getParam('title'); + $description = $this->getRequest()->getParam('description'); + $keywords = (array) $this->getRequest()->getParam('keywords'); + + if ($assetId === 0) { + $responseContent = [ + 'success' => false, + 'message' => __('Image ID is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->updateAsset->execute( + $assetId, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully saved the image "%image"', ['image' => $title]), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to save image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php new file mode 100644 index 0000000000000..e965d94b33f0c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\UploadImage; +use Psr\Log\LoggerInterface; + +/** + * Controller responsible to upload the media gallery content + */ +class Upload extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param UploadImage $upload + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + UploadImage $upload, + LoggerInterface $logger + ) { + parent::__construct($context); + $this->uploadImage = $upload; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $targetFolder = $this->getRequest()->getParam('target_folder'); + $type = $this->getRequest()->getParam('type'); + + if (!$targetFolder) { + $responseContent = [ + 'success' => false, + 'message' => __('The target_folder parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->uploadImage->execute($targetFolder, $type); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('The image was uploaded successfully.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => __('Could not upload image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php new file mode 100644 index 0000000000000..e97d93d86bb0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Index; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\View\Result\Layout; +use Magento\Framework\View\Result\LayoutFactory; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * Index constructor. + * + * @param Context $context + * @param LayoutFactory $layoutFactory + */ + public function __construct( + Context $context, + LayoutFactory $layoutFactory + ) { + parent::__construct($context); + $this->layoutFactory = $layoutFactory; + } + + /** + * Get the media gallery layout + * + * @return Layout + */ + public function execute(): Layout + { + return $this->layoutFactory->create(); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php new file mode 100644 index 0000000000000..3660374243d16 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Media; + +use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * Get the media gallery layout + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->setActiveMenu('Magento_MediaGalleryUi::media_gallery') + ->addBreadcrumb(__('Media'), __('Media Gallery')); + $resultPage->getConfig()->getTitle()->prepend(__('Manage Gallery')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryUi/LICENSE.txt b/app/code/Magento/MediaGalleryUi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php new file mode 100644 index 0000000000000..7c3eccfea521f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset created at date time + */ +class CreatedAt implements AssetDetailsProviderInterface +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @param TimezoneInterface $dateTime + */ + public function __construct( + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + } + + /** + * Provide asset created at date time + * + * @param AssetInterface $asset + * @return array + * @throws \Exception + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Created'), + 'value' => $this->formatDate($asset->getCreatedAt()) + ]; + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + * @throws \Exception + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php new file mode 100644 index 0000000000000..b2b0f389f6b9a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset height + */ +class Height implements AssetDetailsProviderInterface +{ + /** + * Provide asset height + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Height'), + 'value' => sprintf('%spx', $asset->getHeight()) + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php new file mode 100644 index 0000000000000..55841cc5abd3f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset file size + */ +class Size implements AssetDetailsProviderInterface +{ + /** + * Provide asset file size + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Size'), + 'value' => $this->formatImageSize($asset->getSize()) + ]; + } + + /** + * Format image size + * + * @param int $imageSize + * + * @return string + */ + private function formatImageSize(int $imageSize): string + { + if ($imageSize === 0) { + return ''; + } + + return sprintf('%sKb', $imageSize / 1000); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php new file mode 100644 index 0000000000000..5b47616398ef7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset type + */ +class Type implements AssetDetailsProviderInterface +{ + /** + * @var array + */ + private $types; + + /**= + * @param array $types + */ + public function __construct(array $types = []) + { + $this->types = $types; + } + + /** + * Provide asset type + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Type'), + 'value' => $this->getImageTypeByContentType($asset->getContentType()), + ]; + } + + /** + * Return image type by content type + * + * @param string $contentType + * @return string + */ + private function getImageTypeByContentType(string $contentType): string + { + $type = current(explode('/', $contentType)); + + return isset($this->types[$type]) ? $this->types[$type] : 'Asset'; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php new file mode 100644 index 0000000000000..2f50bd9a72208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset updated at date time + */ +class UpdatedAt implements AssetDetailsProviderInterface +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @param TimezoneInterface $dateTime + */ + public function __construct( + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + } + + /** + * Provide asset updated at date time + * + * @param AssetInterface $asset + * @return array + * @throws \Exception + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Modified'), + 'value' => $this->formatDate($asset->getUpdatedAt()) + ]; + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + * @throws \Exception + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php new file mode 100644 index 0000000000000..ca3883d5c937c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide information on which content asset is used in + */ +class UsedIn implements AssetDetailsProviderInterface +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContent; + + /** + * @var array + */ + private $contentTypes; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @param GetContentByAssetIdsInterface $getContent + * @param UrlInterface $url + * @param array $contentTypes + */ + public function __construct( + GetContentByAssetIdsInterface $getContent, + UrlInterface $url, + array $contentTypes = [] + ) { + $this->getContent = $getContent; + $this->url = $url; + $this->contentTypes = $contentTypes; + } + + /** + * Provide information on which content asset is used in + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Used In'), + 'value' => $this->getUsedIn($asset->getId()) + ]; + } + + /** + * Retrieve assets used in the Content + * + * @param int $assetId + * @return array + * @throws IntegrationException + */ + private function getUsedIn(int $assetId): array + { + $details = []; + + foreach ($this->getUsedInCounts($assetId) as $type => $number) { + $details[$type] = $this->contentTypes[$type] ?? ['name' => $type, 'link' => null]; + $details[$type]['number'] = $number; + $details[$type]['link'] = $details[$type]['link'] ? $this->url->getUrl($details[$type]['link']) : null; + } + + return array_values($details); + } + + /** + * Get used in counts per type + * + * @param int $assetId + * @return int[] + * @throws IntegrationException + */ + private function getUsedInCounts(int $assetId): array + { + $usedIn = []; + $entityIds = []; + + $contentIdentities = $this->getContent->execute([$assetId]); + + foreach ($contentIdentities as $contentIdentity) { + $entityId = $contentIdentity->getEntityId(); + $type = $contentIdentity->getEntityType(); + + if (!isset($entityIds[$type])) { + $usedIn[$type] = 1; + } elseif ($entityIds[$type]['entity_id'] !== $entityId) { + ++$usedIn[$type]; + } + $entityIds[$type]['entity_id'] = $entityId; + } + return $usedIn; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php new file mode 100644 index 0000000000000..64e9cf8ad1a8f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset width + */ +class Width implements AssetDetailsProviderInterface +{ + /** + * Provide asset width + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Width'), + 'value' => sprintf('%spx', $asset->getWidth()) + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php new file mode 100644 index 0000000000000..92375adfdd4f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +interface AssetDetailsProviderInterface +{ + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array; +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php new file mode 100644 index 0000000000000..207f35bb99d6a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +class AssetDetailsProviderPool +{ + /** + * @var AssetDetailsProviderInterface[] + */ + private $detailsProviders; + + /** + * @param AssetDetailsProviderInterface[] $detailsProviders + */ + public function __construct(array $detailsProviders = []) + { + $this->detailsProviders = $detailsProviders; + } + + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array + { + $details = []; + foreach ($this->detailsProviders as $detailsProvider) { + if ($detailsProvider instanceof AssetDetailsProviderInterface) { + $details[] = $detailsProvider->execute($asset); + } + } + return $details; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Config.php b/app/code/Magento/MediaGalleryUi/Model/Config.php new file mode 100644 index 0000000000000..a9391d76428ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Config.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Class responsible to provide access to system configuration related to the Media Gallery + */ +class Config implements ConfigInterface +{ + /** + * Path to enable/disable media gallery in the system settings. + */ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if masonry grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php new file mode 100644 index 0000000000000..2f4793c28ad47 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Delete image from a storage + */ +class DeleteImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * DeleteImage constructor. + * + * @param Storage $imagesStorage + * @param Filesystem $filesystem + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem, + IsPathExcludedInterface $isPathExcluded + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Delete asset image physically from file storage and from data storage. + * + * @param AssetInterface[] $assets + * @throws LocalizedException + */ + public function execute(array $assets): void + { + $failedAssets = []; + foreach ($assets as $asset) { + if ($this->isPathExcluded->execute($asset->getPath())) { + $failedAssets[] = $asset->getPath(); + } + + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $absolutePath = $mediaDirectory->getAbsolutePath($asset->getPath()); + $this->imagesStorage->deleteFile($absolutePath); + } + if (!empty($failedAssets)) { + throw new LocalizedException( + __( + 'Could not delete "%image": destination directory is restricted.', + ['image' => implode(",", $failedAssets)] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php new file mode 100644 index 0000000000000..574b8aab8bcd3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Directories; + +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Build folder tree structure by path + */ +class FolderTree +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $path; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * Constructor + * + * @param Filesystem $filesystem + * @param string $path + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Filesystem $filesystem, + string $path, + IsPathExcludedInterface $isPathExcluded + ) { + $this->filesystem = $filesystem; + $this->path = $path; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Return directory folder structure in array + * + * @param bool $skipRoot + * @return array + * @throws ValidatorException + */ + public function buildTree(bool $skipRoot = true): array + { + return $this->buildFolderTree($this->getDirectories(), $skipRoot); + } + + /** + * Build directory tree array in format for jstree strandart + * + * @return array + * @throws ValidatorException + */ + private function getDirectories(): array + { + $directories = []; + + /** @var Read $directory */ + $directory = $this->filesystem->getDirectoryRead($this->path); + + if (!$directory->isDirectory()) { + return $directories; + } + + foreach ($directory->readRecursively() as $path) { + if (!$directory->isDirectory($path) || $this->isPathExcluded->execute($path)) { + continue; + } + + $pathArray = explode('/', $path); + $directories[] = [ + 'data' => count($pathArray) > 0 ? end($pathArray) : $path, + 'attr' => ['id' => $path], + 'metadata' => [ + 'path' => $path + ], + 'path_array' => $pathArray + ]; + } + return $directories; + } + + /** + * Build folder tree structure by provided directories path + * + * @param array $directories + * @param bool $skipRoot + * @return array + */ + private function buildFolderTree(array $directories, bool $skipRoot): array + { + $tree = [ + 'name' => 'root', + 'path' => '/', + 'children' => [] + ]; + foreach ($directories as $idx => &$node) { + $node['children'] = []; + $result = $this->findParent($node, $tree); + $parent = & $result['treeNode']; + + $parent['children'][] =& $directories[$idx]; + } + return $skipRoot ? $tree['children'] : $tree; + } + + /** + * Find parent directory + * + * @param array $node + * @param array $treeNode + * @param int $level + * @return array + */ + private function findParent(array &$node, array &$treeNode, int $level = 0): array + { + $nodePathLength = count($node['path_array']); + $treeNodeParentLevel = $nodePathLength - 1; + + $result = ['treeNode' => &$treeNode]; + + if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) { + return $result; + } + + foreach ($treeNode['children'] as &$tnode) { + if ($node['path_array'][$level] === $tnode['path_array'][$level]) { + return $this->findParent($node, $tnode, $level + 1); + } + } + return $result; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php new file mode 100644 index 0000000000000..b870082ea2aa1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Exception; +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Load Media Asset from database by id add all related data to it + */ +class GetDetailsByAssetId +{ + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsById; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var SourceIconProvider + */ + private $sourceIconProvider; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @var AssetDetailsProviderPool + */ + private $detailsProviderPool; + + /** + * @param AssetDetailsProviderPool $detailsProviderPool + * @param GetAssetsByIdsInterface $getAssetById + * @param StoreManagerInterface $storeManager + * @param SourceIconProvider $sourceIconProvider + * @param GetAssetsKeywordsInterface $getAssetKeywords + */ + public function __construct( + AssetDetailsProviderPool $detailsProviderPool, + GetAssetsByIdsInterface $getAssetById, + StoreManagerInterface $storeManager, + SourceIconProvider $sourceIconProvider, + GetAssetsKeywordsInterface $getAssetKeywords + ) { + $this->detailsProviderPool = $detailsProviderPool; + $this->getAssetsById = $getAssetById; + $this->storeManager = $storeManager; + $this->sourceIconProvider = $sourceIconProvider; + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * Get image details by assets Ids + * + * @param array $assetIds + * @throws LocalizedException + * @throws Exception + * @return array + */ + public function execute(array $assetIds): array + { + $assets = $this->getAssetsById->execute($assetIds); + + $details = []; + foreach ($assets as $asset) { + $details[$asset->getId()] = [ + 'image_url' => $this->getUrl($asset->getPath()), + 'title' => $asset->getTitle(), + 'path' => $asset->getPath(), + 'description' => $asset->getDescription(), + 'id' => $asset->getId(), + 'details' => $this->detailsProviderPool->execute($asset), + 'size' => $asset->getSize(), + 'tags' => $this->getKeywords($asset), + 'source' => $asset->getSource() ? + $this->sourceIconProvider->getSourceIconUrl($asset->getSource()) : + null, + 'content_type' => strtoupper(str_replace('image/', '', $asset->getContentType())), + ]; + } + return $details; + } + + /** + * Key asset keywords + * + * @param AssetInterface $asset + * @return string[] + */ + private function getKeywords(AssetInterface $asset): array + { + $assetKeywords = $this->getAssetKeywords->execute([$asset->getId()]); + + if (empty($assetKeywords)) { + return []; + } + + $keywords = current($assetKeywords)->getKeywords(); + + return array_map( + function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, + $keywords + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * + * @return string + * + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $path; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..88401465d56b7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Listing; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider as UiComponentDataProvider; +use Magento\MediaGalleryUi\Ui\Component\Listing\Provider; + +/** + * Media gallery UI data provider. Try catch added for displaying errors in grid + */ +class DataProvider extends UiComponentDataProvider +{ + /** + * @var CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory $collectionFactory + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $collectionFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->collectionFactory = $collectionFactory; + $this->collectionProcessor = $collectionProcessor; + } + + /** + * @inheritdoc + */ + public function getData(): array + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + /** @var Provider $collection */ + $collection = $this->collectionFactory->getReport($this->getSearchCriteria()->getRequestName()); + $this->collectionProcessor->process($this->getSearchCriteria(), $collection); + + return $collection; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php new file mode 100644 index 0000000000000..785c3078cdbe5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to filter a content field + */ +class ContentField implements CustomFilterInterface +{ + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentStatus; + + /** + * ContentField constructor. + * + * @param GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + */ + public function __construct( + GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + ) { + $this->getAssetIdsByContentStatus = $getAssetIdsByContentStatus; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $collection->addFieldToFilter( + 'main_table.id', + ['in' => $this->getAssetIdsByContentStatus->execute($filter->getField(), $filter->getValue())] + ); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php new file mode 100644 index 0000000000000..36e9375525f8d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; + +class Directory implements CustomFilterInterface +{ + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = str_replace('%', '', $filter->getValue()); + $collection->getSelect()->where('path REGEXP ? ', '^' . $value . '/[^\/]*$'); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php new file mode 100644 index 0000000000000..d43b3ac2ca451 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +/** + * Custom filter to filter collection by duplicated hash values + */ +class Duplicated implements CustomFilterInterface +{ + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + if ($filter->getValue()) { + $collection->getSelect()->where('main_table.hash IN (?)', $this->getDuplicatedIds()); + } + return true; + } + /** + * Return sql part of duplicated values. + */ + private function getDuplicatedIds(): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select() + ->from($this->connection->getTableName('media_gallery_asset'), ['hash']) + ->group('hash') + ->having('COUNT(*) > 1') + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php new file mode 100644 index 0000000000000..1d8aa78a03cc2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +/** + * Custom filter to filter collection by entity type + */ +class Entity implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var string + */ + private $entityType; + + /** + * @param ResourceConnection $resource + * @param string $entityType + */ + public function __construct(ResourceConnection $resource, string $entityType) + { + $this->connection = $resource; + $this->entityType = $entityType; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $ids = $filter->getValue(); + if (is_array($ids)) { + $collection->addFieldToFilter( + self::TABLE_ALIAS . '.id', + ['in' => $this->getSelectByEntityIds($ids)] + ); + } + return true; + } + + /** + * Return select asset ids by entity type + * + * @param array $ids + * @return Select + */ + private function getSelectByEntityIds(array $ids): Select + { + return $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->where( + 'entity_id IN (?)', + $ids + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php new file mode 100644 index 0000000000000..1b5e2282ff3dc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +/** + * Custom filter to filter collection by entity type + */ +class EntityType implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const NOT_USED = 'not_used'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + if (is_array($value)) { + $conditions = []; + + if (in_array(self::NOT_USED, $value)) { + unset($value[array_search(self::NOT_USED, $value)]); + $conditions[] = ['in' => $this->getNotUsedEntityIds()]; + } + + if (!empty($value)) { + $conditions[] = ['in' => $this->getEntityTypesIds($value)]; + } + + $collection->addFieldToFilter( + self::TABLE_ALIAS . '.id', + $conditions + ); + } + return true; + } + + /** + * Return asset ids by entity type + * + * @param array $value + * @return array + */ + private function getEntityTypesIds(array $value): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type IN (?)', + $value + ) + ); + } + + /** + * Return asset ids that not exists in asset_content_table + */ + private function getNotUsedEntityIds(): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + ['media_gallery_asset' => $this->connection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET)], + ['id'] + )->where( + 'media_gallery_asset.id not in ?', + $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + ) + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php new file mode 100644 index 0000000000000..a3003e3f5a23a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +class Keyword implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_KEYWORDS = 'media_gallery_asset_keyword'; + private const TABLE_ASSET_KEYWORD = 'media_gallery_keyword'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + + $collection->addFieldToFilter( + [self::TABLE_ALIAS . '.title', self::TABLE_ALIAS . '.id'], + [ + ['like' => sprintf('%%%s%%', $value)], + ['in' => $this->getAssetIdsByKeyword($value)] + ] + ); + + return true; + } + + /** + * Return asset ids by keyword + * + * @param string $value + * @return array + */ + private function getAssetIdsByKeyword(string $value): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select()->from( + $connection->select() + ->from( + ['asset_keywords_table' => $this->connection->getTableName(self::TABLE_ASSET_KEYWORD)], + ['id'] + )->where( + 'keyword = ?', + $value + )->joinInner( + ['keywords_table' => $this->connection->getTableName(self::TABLE_KEYWORDS)], + 'keywords_table.keyword_id = asset_keywords_table.id', + ['asset_id'] + ), + ['asset_id'] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php new file mode 100644 index 0000000000000..ff82b990d2a01 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryUi\Model\UpdateAsset\UpdateKeywords; +use Magento\MediaGalleryUi\Model\UpdateAsset\SaveMetadataToFile; + +class UpdateAsset +{ + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var SaveMetadataToFile + */ + private $processMetadata; + + /** + * @var UpdateKeywords + */ + private $processKeywords; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param SaveAssetsInterface $saveAssets + * @param UpdateKeywords $processKeywords + * @param SaveMetadataToFile $processMetadata + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByIdsInterface $getAssetsByIds, + SaveAssetsInterface $saveAssets, + UpdateKeywords $processKeywords, + SaveMetadataToFile $processMetadata + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByIds = $getAssetsByIds; + $this->saveAssets = $saveAssets; + $this->processKeywords = $processKeywords; + $this->processMetadata = $processMetadata; + } + + /** + * Save asset details + * + * @param int $id + * @param MetadataInterface $data + */ + public function execute(int $id, MetadataInterface $data): void + { + $asset = $this->getAsset($id); + + $updatedAsset = $this->assetFactory->create( + [ + 'path' => $asset->getPath(), + 'contentType' => $asset->getContentType(), + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + 'size' => $asset->getSize(), + 'id' => $asset->getId(), + 'title' => $data->getTitle() ?? $asset->getTitle(), + 'description' => $data->getDescription() ?? $asset->getDescription(), + 'source' => $asset->getSource(), + 'hash' => $asset->getHash(), + 'created_at' => $asset->getCreatedAt(), + 'updated_at' => $asset->getUpdatedAt() + ] + ); + + $this->saveAssets->execute([$updatedAsset]); + $this->processMetadata->execute($asset->getPath(), $data); + + $keywords = $data->getKeywords(); + if (isset($keywords)) { + $this->processKeywords->execute($id, $keywords); + } + } + + /** + * Load asset by id + * + * @param int $id + * @return AssetInterface + * @throws LocalizedException + */ + private function getAsset(int $id): AssetInterface + { + $assets = $this->getAssetsByIds->execute([$id]); + if (empty($assets)) { + throw new LocalizedException(__('Could not retrieve the asset.')); + } + return current($assets); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php new file mode 100644 index 0000000000000..3ebe04374f81e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Psr\Log\LoggerInterface; + +class SaveMetadataToFile +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param AddMetadataInterface $addMetadata + * @param LoggerInterface $logger + */ + public function __construct( + Filesystem $filesystem, + AddMetadataInterface $addMetadata, + LoggerInterface $logger + ) { + $this->filesystem = $filesystem; + $this->addMetadata = $addMetadata; + $this->logger = $logger; + } + + /** + * Save updated metadata + * + * @param string $path + * @param MetadataInterface $data + */ + public function execute(string $path, MetadataInterface $data): void + { + $absolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path); + + try { + $this->addMetadata->execute($absolutePath, $data); + } catch (LocalizedException $e) { + $this->logger->critical($e); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php new file mode 100644 index 0000000000000..2a359d5a14025 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; + +class UpdateKeywords +{ + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param KeywordInterfaceFactory $keywordFactory + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + */ + public function __construct( + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + KeywordInterfaceFactory $keywordFactory, + SaveAssetsKeywordsInterface $saveAssetKeywords + ) { + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->keywordFactory = $keywordFactory; + $this->saveAssetKeywords = $saveAssetKeywords; + } + + /** + * Save asset keywords + * + * @param int $assetId + * @param string[] $keywords + */ + public function execute(int $assetId, array $keywords): void + { + $this->saveAssetKeywords->execute([ + $this->assetKeywordsFactory->create([ + 'assetId' => $assetId, + 'keywords' => $this->createKeywords($keywords) + ]) + ]); + } + + /** + * Create keyword objects from strings + * + * @param string[] $keywords + * @return KeywordInterface[] + */ + private function createKeywords(array $keywords): array + { + $keywordObjects = []; + foreach ($keywords as $keyword) { + $keywordObjects[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + return $keywordObjects; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UploadImage.php b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php new file mode 100644 index 0000000000000..c918548bea553 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; + +/** + * Uploads an image to storage + */ +class UploadImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Storage $imagesStorage + * @param Filesystem $filesystem + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + } + + /** + * Uploads the image and returns file object + * + * @param string $targetFolder + * @param string $type + * @throws LocalizedException + */ + public function execute(string $targetFolder, string $type): void + { + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + if (!$mediaDirectory->isDirectory($targetFolder)) { + throw new LocalizedException(__('Directory %1 does not exist in media directory.', $targetFolder)); + } + + $this->imagesStorage->uploadFile($mediaDirectory->getAbsolutePath($targetFolder), $type); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php new file mode 100644 index 0000000000000..7988ac2d9e635 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Plugin; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite; + +/** + * Create resizes files that were synced + */ +class CreateThumbnails +{ + /** + * @var Storage + */ + private $storage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + * @param Storage $storage + */ + public function __construct(Filesystem $filesystem, Storage $storage) + { + $this->storage = $storage; + $this->filesystem = $filesystem; + } + + /** + * Create thumbnails for synced files. + * + * @param ImportFilesComposite $subject + * @param string[] $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(ImportFilesComposite $subject, array $paths): array + { + foreach ($paths as $path) { + $this->storage->resizeFile( + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path) + ); + } + + return [$paths]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md new file mode 100644 index 0000000000000..6fbad656b23a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUi module + +The Magento_MediaGalleryUi module is responsible for the media gallery user interface (UI) implementation. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..c056727aa8fe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertImageInStandaloneMediaGalleryActionGroup"> + <annotations> + <description>Validates that the provided image is present and correct in the standalone media gallery.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + + <seeElement selector="{{AdminEnhancedMediaGalleryActionsSection.imageSrc(imageName)}}" + stepKey="checkFirstImageAfterSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..d47eb491f9b5d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup"> + <annotations> + <description>Adds image to target element from View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.addImage}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml new file mode 100644 index 0000000000000..9a550805a7dec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup"> + <annotations> + <description>Applies duplicated images filter to the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.duplicatedFilterCheckbox}}" stepKey="clickShowDuplicates"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..9d7d725cf49de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.applyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml new file mode 100644 index 0000000000000..aeee921f92e58 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup"> + <annotations> + <description>Assert media gallery grid filters</description> + </annotations> + <arguments> + <argument name="resultValue" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFiltersToCheckAppliedFilter"/> + <see selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilter(resultValue)}}" userInput="{{resultValue}}" stepKey="verifyAppliedFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml new file mode 100644 index 0000000000000..7f4db971702ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup"> + <annotations> + <description>Asserts images has been deleted in mass action.</description> + </annotations> + + <see userInput='Assets have been successfully deleted' stepKey="verifyDeleteImages"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml new file mode 100644 index 0000000000000..efcf40cd2b644 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup"> + <annotations> + <description>Asserts that massaction mode can be enabled and disabled, verify massaction view after switch to massaction mode</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + <see selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" userInput="(1 Selected)" stepKey="verifySelectedCount"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml new file mode 100644 index 0000000000000..a691f65387e8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup"> + <annotations> + <description>Asserts that massaction mode is terminated</description> + </annotations> + + + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMassAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml new file mode 100644 index 0000000000000..783e71719c659 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup"> + <annotations> + <description>Assert that grid have no active filter</description> + </annotations> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilterPlaceholder}}" stepKey="assertThereIsNoActiveFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml new file mode 100644 index 0000000000000..b53e76e06cfb5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup"> + <annotations> + <description>Assert image delete action popup contains warnin message</description> + </annotations> + <arguments> + <argument name="messageText" type="string"/> + </arguments> + + <see userInput="{{messageText}}" stepKey="assertWarningMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..478ca2b3b5be9 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in Category grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridApplyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..00608504fd7a6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery category filters by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridFiltersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml new file mode 100644 index 0000000000000..600e1cd747943 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup"> + <annotations> + <description>Click delete images button.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="clickDeleteImages"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml new file mode 100644 index 0000000000000..3754eb319da44 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup"> + <annotations> + <description>Closes View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.cancel}}" stepKey="clickCancel"/> + <wait time="1" stepKey="waitForElementRender"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml new file mode 100644 index 0000000000000..90546eca8dc0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup"> + <annotations> + <description>Click confirm on confirmation popup images delete action.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeletingProcces"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml new file mode 100644 index 0000000000000..d3d1f0aaf39de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryDeleteGridViewActionGroup"> + <annotations> + <description>Delete grid view bookmarks by name</description> + </annotations> + <arguments> + <argument name="viewToDelete" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(viewToDelete)}}{{AdminAdobeStockSection.editViewButtonPartial}}" stepKey="clickEditButton"/> + <seeElement selector="{{AdminAdobeStockSection.deleteViewButton}}" stepKey="seeDeleteButton"/> + <click selector="{{AdminAdobeStockSection.deleteViewButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForDeletion" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml new file mode 100644 index 0000000000000..f404ffbe7c4f0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup"> + <annotations> + <description>Disable massaction mode by clicking on cancel button</description> + </annotations> + + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.cancelMassActionMode}}" stepKey="cancelMassAction"/> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMAssAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..84712e8e3f3ae --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryEditImageDetailsActionGroup"> + <annotations> + <description>Opens Edit image details panel panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.edit}}" stepKey="edit"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml new file mode 100644 index 0000000000000..4b0375088509b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup"> + <annotations> + <description>Activate massaction mode by click on Delete Selected..</description> + </annotations> + + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="waitForMassActionButton"/> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="clickOnMassActionButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..d2ac1c78b2582 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery filter by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..b3733ceb4c4a0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Media Gallery</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.delete}}" stepKey="deleteImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml new file mode 100644 index 0000000000000..001aa010dbdd4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup"> + <annotations> + <description>Delete image from the View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.delete}}" stepKey="deleteImage"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="waitForConfirmation"/> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml new file mode 100644 index 0000000000000..931da0ee06fef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDetailsEditActionGroup"> + <annotations> + <description>Edit image from the View Details panel</description> + </annotations> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml new file mode 100644 index 0000000000000..0da3de9501c13 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup"> + <annotations> + <description>Save image details from the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.title}}" userInput="{{image.title}}" stepKey="setTitle" /> + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.description}}" userInput="{{image.description}}" stepKey="setDescription" /> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.save}}" stepKey="saveDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml new file mode 100644 index 0000000000000..57096124c0370 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySaveCustomViewActionGroup"> + <annotations> + <description>Save custom view media gallery</description> + </annotations> + <arguments> + <argument name="viewName" type="string" defaultValue="Test View"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.saveViewAs}}" stepKey="saveView"/> + <fillField selector="{{AdminGridDefaultViewControls.viewName}}" userInput="{{viewName}}" stepKey="inputViewName"/> + <pressKey selector="{{AdminGridDefaultViewControls.viewName}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml new file mode 100644 index 0000000000000..4244724599fed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup"> + <annotations> + <description>Apply custom bookmarks view to the media gallery grid</description> + </annotations> + <arguments> + <argument name="selectView" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(selectView)}}" stepKey="clickOnViewButton"/> + <waitForPageLoad stepKey="waitForGridLoad" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml new file mode 100644 index 0000000000000..6532fb869d2cc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup"> + <annotations> + <description>Select images in grid by clicking on mass action checkbox</description> + </annotations> + <arguments> + <argument name="imageName" type="string" defaultValue="magento"/> + </arguments> + + <checkOption selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml new file mode 100644 index 0000000000000..9be288b064742 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectSourceFilterActionGroup"> + <annotations> + <description>Select source filter by provided option</description> + </annotations> + <arguments> + <argument type="string" name="filterValue"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.sourceFilterValue(filterValue)}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..72d01e1871513 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup"> + <annotations> + <description>Set search options filter</description> + </annotations> + <arguments> + <argument type="string" name="filterName"/> + <argument type="string" name="optionName"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilter(filterName)}}" stepKey="openFilter"/> + <fillField selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterInput(filterName)}}" userInput="{{optionName}}" stepKey="enterOptionName" /> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterOption(filterName, optionName)}}" stepKey="selectOption"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone(filterName)}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml new file mode 100644 index 0000000000000..053a1185b3fda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryUploadImageActionGroup"> + <annotations> + <description>Uploads the provided Image to Media Gallery. + If you use this action group, you MUST add steps to delete the image in the "after" steps.</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <attachFile selector="{{AdminEnhancedMediaGalleryActionsSection.upload}}" userInput="{{image.value}}" stepKey="uploadImage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml new file mode 100644 index 0000000000000..eb2fc79567d08 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup"> + <annotations> + <description>Verifies image description on the View Details panel</description> + </annotations> + <arguments> + <argument name="description"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.description}}" stepKey="grabDescription"/> + <assertStringContainsString stepKey="verifyDescription"> + <actualResult type="variable">grabDescription</actualResult> + <expectedResult type="string">{{description}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..1ebaa0581e33e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup"> + <annotations> + <description>Verifies image information on the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.type}}" stepKey="grabType"/> + <assertStringContainsString stepKey="verifyType"> + <actualResult type="variable">grabType</actualResult> + <expectedResult type="string">Image</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml new file mode 100644 index 0000000000000..4c38b7dbc8c3e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup"> + <annotations> + <description>Verifies image filename on the View Details panel</description> + </annotations> + <arguments> + <argument name="filename" type="string"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.filename}}" stepKey="grabFilename"/> + <assertStringContainsString stepKey="verifyFilename"> + <actualResult type="variable">grabFilename</actualResult> + <expectedResult type="string">{{filename}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml new file mode 100644 index 0000000000000..2fc4f7ea25fd0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup"> + <annotations> + <description>Verifies image keywords on the View Details panel</description> + </annotations> + <arguments> + <argument name="keywords"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> + <assertStringContainsString stepKey="verifyKeywords"> + <actualResult type="variable">grabKeywords</actualResult> + <expectedResult type="string">{{keywords}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml new file mode 100644 index 0000000000000..08dac976332ee --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup"> + <annotations> + <description>Verifies image title on the View Details panel</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..b5c0bbac69bec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryViewImageDetails"> + <annotations> + <description>Opens View Details panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.viewDetails}}" stepKey="viewDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml new file mode 100644 index 0000000000000..6ddb6311c1a7e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryApplySelectFilterActionGroup"> + <annotations> + <description>Applies select filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="filterLabel" type="string"/> + <argument name="optionLabel" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilter(filterLabel)}}" stepKey="openSelectFilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilterOption(filterLabel, optionLabel)}}" stepKey="selectFilterOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..a930f65b71040 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryApplyUsedInFilterActionGroup"> + <annotations> + <description>Applies Show Images Used In filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="entityType" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInSelectDropdown}}" stepKey="openUsedInfilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInEntityType(entityType)}}" stepKey="selectEntityType"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone('Show Images Used In')}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml new file mode 100644 index 0000000000000..42d723f0811d3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup"> + <annotations> + <description>Asserts category name in category grid page</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', categoryName)}}" stepKey="assertNameColumn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml new file mode 100644 index 0000000000000..d0d9817da6d34 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertFolderDoesNotExistActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="5" stepKey="waitForFolderTreeReloads"/> + <dontSeeElement selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="folderDoesNotExist"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml new file mode 100644 index 0000000000000..7d71c764bc8de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertFolderNameActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <waitForElementVisible selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="waitForFolder"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml new file mode 100644 index 0000000000000..6785558c8ef54 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertImageInGridActionGroup"> + <annotations> + <description>Asserts that image exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml new file mode 100644 index 0000000000000..cc4de51357de0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup"> + <annotations> + <description>Asserts that image does not exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml new file mode 100644 index 0000000000000..28dcc1c553a5a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryClickAddSelectedActionGroup"> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="waitForAddSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="ClickAddSelected"/> + <wait time="5" stepKey="waitForImageToBeAdded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml new file mode 100644 index 0000000000000..ee2ff887488a4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryClickImageInGridActionGroup"> + <annotations> + <description>Select image on enhanced media gallery</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="waitForImageToBeVisible"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="clickOnImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml new file mode 100644 index 0000000000000..3e555c25e0a98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup"> + <annotations> + <description>Click ok button on upload image tinyMce4 popup.</description> + </annotations> + + <waitForElementVisible selector="{{MediaGallerySection.OkBtn}}" stepKey="waitForOkBtn"/> + <click selector="{{MediaGallerySection.OkBtn}}" stepKey="clickOkBtn"/> + <waitForPageLoad stepKey="wait"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml new file mode 100644 index 0000000000000..f3ccc8ef7be04 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryCreateNewFolderActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <fillField selector="{{AdminMediaGalleryFolderSection.folderNameField}}" userInput="{{name}}" stepKey="setFolderName" /> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmCreateButton}}" stepKey="clickCreateButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml new file mode 100644 index 0000000000000..964b33dd38d55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryEditAssetAddKeywordActionGroup"> + <annotations> + <description>Set Keywords on the Edit Details panel</description> + </annotations> + <arguments> + <argument name="keyword"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.newKeyword}}" userInput="{{keyword}}" stepKey="enterKeyword"/> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.addNewKeyword}}" stepKey="addKeyword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml new file mode 100644 index 0000000000000..8791c1b152249 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryEnhancedEnableActionGroup"> + <arguments> + <argument name="enabled" type="string" defaultValue="{{MediaGalleryConfigDataDisabled.value}}"/> + </arguments> + <amOnPage url="{{AdminConfigSystemPage.url}}" stepKey="navigateToSystemConfigurationPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <scrollTo selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="scrollToEnhancedMediaGalleryFieldset"/> + <conditionalClick stepKey="expandEnhancedMediaGalleryTab" selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" dependentSelector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" visible="false" /> + <waitForElementVisible selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="waitForFieldset" /> + <selectOption userInput="{{enabled}}" selector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" stepKey="enableOrDisableMediaGallery"/> + <click selector="{{AdminConfigSystemSection.saveConfig}}" stepKey="saveConfiguration"/> + <waitForPageLoad stepKey="waitForConfigurationToSave"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml new file mode 100644 index 0000000000000..f7e8f551e681f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryFolderDeleteActionGroup"> + <wait time="2" stepKey="waitBeforeDeleteButtonWillBeActive"/> + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderDeleteModalHeader}}" stepKey="waitBeforeModalAppears"/> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmDeleteButton}}" stepKey="clickConfirmDeleteButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml new file mode 100644 index 0000000000000..b8ed1d4f1cd25 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryFolderSelectActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="selectFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..e6cbbfbc1f48d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Enhanced Media Gallery using header delete button</description> + </annotations> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="waitForDeleteSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="ClickDeleteSelectedButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml new file mode 100644 index 0000000000000..165522892f271 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryOpenNewFolderFormActionGroup"> + <click selector="{{AdminMediaGalleryFolderSection.folderNewCreateButton}}" stepKey="clickCreateNewFolderButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderNewModalHeader}}" stepKey="waitForModalOpen"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml new file mode 100644 index 0000000000000..6f38bd7c7d738 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from image uploader on category page</description> + </annotations> + + <conditionalClick stepKey="clickExpandContent" selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.selectFromGalleryButton}}" visible="false" /> + <waitForElementVisible selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="waitForSelectFromGallery" /> + <click selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="clickSelectFromGallery" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml new file mode 100644 index 0000000000000..0b2540de5288e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryFromPageNoEditorActionGroup"> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="waitForInsertImageButton" /> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImage" /> + <!-- wait for initial media gallery load, where the gallery chrome loads (and triggers loading modal) --> + <waitForPageLoad stepKey="waitForMediaGalleryInitialLoad"/> + <!-- wait for second media gallery load, where the gallery images load (and triggers loading modal once more) --> + <waitForPageLoad stepKey="waitForMediaGallerySecondaryLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml new file mode 100644 index 0000000000000..3143b4ff24fb4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryTinyMce4ActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from category page by tyniMce4 image icon</description> + </annotations> + + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowse"/> + <waitForPageLoad stepKey="waitForPopup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..1ef908f34918e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenStandaloneMediaGalleryActionGroup"> + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="amOnStandaloneMediaGalleryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml new file mode 100644 index 0000000000000..e9558ac87df3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup"> + <annotations> + <description>Assert that an image was deleted from Enhanced Media Gallery.</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <see userInput='The asset "{{title}}" has been successfully deleted' stepKey="verifyDeleteImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml new file mode 100644 index 0000000000000..ff11f1a5c7058 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertImageAddedToPageContentActionGroup"> + <annotations> + <description>Validates that the an image was added to the content.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <grabValueFrom selector="{{CmsNewPagePageContentSection.content}}" stepKey="grabTextFromContent"/> + <assertStringContainsString stepKey="assertContentContainsAddedImage"> + <expectedResult type="string">{{imageName}}</expectedResult> + <actualResult type="variable">grabTextFromContent</actualResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..e17be216335fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertImageAttributesOnEnhancedMediaGalleryActionGroup"> + <annotations> + <description>Assets image information on the Media Gallery grid</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.dimensions}}" stepKey="grabDimensions"/> + <assertNotEmpty stepKey="verifyDimensions"> + <actualResult type="variable">grabDimensions</actualResult> + </assertNotEmpty> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml new file mode 100644 index 0000000000000..1d568fb6a1da4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup" extends="SearchAdminDataGridByKeywordActionGroup"> + <annotations> + <description>EXTENDS: SearchAdminDataGridByKeywordActionGroup. Fills 'Search by keyword' on an Standalone Media Gallery Admin Grid page. Clicks on Submit Search.</description> + </annotations> + <arguments> + <argument name="keyword" type="string" defaultValue=""/> + </arguments> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml new file mode 100644 index 0000000000000..1ec5e7d802a61 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup"> + <arguments> + <argument name="categoryEntity" defaultValue="SimpleSubCategory"/> + <argument name="imageName" type="string"/> + </arguments> + <annotations> + <description>Navigates to the category page on the storefront and asserts that the image is present in description.</description> + </annotations> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="openHomePage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryEntity.name)}}" stepKey="toCategory"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.imageSource(imageName)}}" stepKey="seeImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml new file mode 100644 index 0000000000000..dbc298798ee8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="UpdatedImageDetails" type="image"> + <data key="title">renamed title</data> + <data key="description">test description</data> + <data key="file">magento.jpg</data> + <data key="fileName">renamed title</data> + <data key="extension">jpg</data> + <data key="keyword">newkeyword</data> + </entity> + <entity name="ImageUploadPng" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">png.png</data> + <data key="file">png.png</data> + <data key="fileName">png</data> + <data key="extension">png</data> + </entity> + <entity name="ImageUploadGif" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">gif.gif</data> + <data key="file">gif.gif</data> + <data key="fileName">gif</data> + <data key="extension">gif</data> + </entity> + <entity name="ImageMetadata" type="image"> + <data key="title">Title of the magento image</data> + <data key="description">Description of the magento image</data> + <data key="file">magento3.jpg</data> + <data key="fileName">Title of the magento image</data> + <data key="extension">jpg</data> + <data key="keywords">magento, mediagallerymetadata</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml new file mode 100644 index 0000000000000..e4149acdf58d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMediaGalleryFolderData"> + <data key="name" unique="suffix">folder</data> + </entity> + <entity name="AdminMediaGalleryFolderInvalidData"> + <data key="name">,.?/:;'[{]}|~`!@#$%^*()_=+</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml new file mode 100644 index 0000000000000..e8f394a006104 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MediaGalleryConfigDataEnabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">1</data> + </entity> + <entity name="MediaGalleryConfigDataDisabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml new file mode 100644 index 0000000000000..f7ed27171db40 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminStandaloneMediaGalleryPage" url="/media_gallery/media" area="admin" module="Magento_MediaGalleryUi"/> +</pages> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml new file mode 100644 index 0000000000000..e0305a8a6f172 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminConfigSystemSection"> + <element name="enhancedMediaGalleryFieldset" type="block" selector="#system_media_gallery-head"/> + <element name="enhancedMediaGalleryEnabledField" type="select" selector="[data-ui-id='select-groups-media-gallery-fields-enabled-value']"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml new file mode 100644 index 0000000000000..2feaaa8594da2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryActionsSection"> + <element name="upload" type="input" selector="#image-uploader-input"/> + <element name="cancel" type="button" selector="[data-ui-id='cancel-button']"/> + <element name="createFolder" type="button" selector="[data-ui-id='create-folder-button']"/> + <element name="deleteFolder" type="button" selector="[data-ui-id='delete-folder-button']"/> + <element name="imageSrc" type="text" selector="//div[@class='masonry-image-column' and contains(@data-repeat-index, '0')]//img[contains(@src,'{{src}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml new file mode 100644 index 0000000000000..b4071295bacf3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryDeleteModalSection"> + <element name="confirmDelete" type="button" selector=".media-gallery-delete-image-action .action-accept"/> + <element name="cancelDelete" type="button" selector=".media-gallery-delete-image-action .action-dismiss"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml new file mode 100644 index 0000000000000..b8e2f698ccfe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryEditDetailsSection"> + <element name="title" type="input" selector="#title"/> + <element name="fileName" type="text" selector="#path"/> + <element name="description" type="textarea" selector="#description"/> + <element name="newKeyword" type="input" selector="[data-ui-id='keyword']"/> + <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + <element name="save" type="button" selector="#image-details-action-save"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml new file mode 100644 index 0000000000000..32b109f1e0483 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryFiltersSection"> + <element name="filtersButton" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-expand']"/> + <element name="categoryGridFiltersButton" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-expand']"/> + <element name="sourceFilterValue" type="select" parameterized="true" selector="//div[@class='media-gallery-container']//select[@name='source']//option[@value='{{option}}']"/> + <element name="applyFilters" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-apply']"/> + <element name="categoryGridApplyFilters" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-apply']"/> + <element name="activeFilter" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']//span[contains( ., '{{filter}}')]" parameterized="true"/> + <element name="activeFilterPlaceholder" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']"/> + <element name="usedInSelectDropdown" type="text" selector="//label[@class='admin__form-field-label']/span[text()='Show Images Used In']/parent::*/parent::div/div//div[@class='admin__action-multiselect-text' and text()='Select...']"/> + <element name="usedInEntityType" type="text" selector="//label[@class='admin__action-multiselect-label']/span[text()='{{entityType}}']" parameterized="true"/> + <element name="usedInDoneButton" type="button" selector="//div[@class='admin__action-multiselect-actions-wrap']/button/span[text()='Done']"/> + <element name="selectFilter" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select" parameterized="true"/> + <element name="selectFilterOption" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select/option[@data-title='{{optionLabel}}']" parameterized="true"/> + <element name="searchOptionsFilter" type="select" selector="//div[label/span[contains(text(), '{{filterName}}')]]//div[@class='action-select admin__action-multiselect']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterInput" type="input" selector="//div[label/span[contains(text(), '{{filterName}}')]]//input[@data-role='advanced-select-text']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterOption" type="text" selector="//div[label/span[contains(text(), '{{filterName}}')]]//label[@class='admin__action-multiselect-label']/span[text()='{{optionName}}']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterDone" type="button" selector="//div[label/span[contains(text(), '{{filterName}}')]]//button[@data-action='close-advanced-select']" parameterized="true"/> + <element name="duplicatedFilterCheckbox" type="button" selector="//input[@name='duplicated']"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml new file mode 100644 index 0000000000000..3f13a57697e6f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryImageActionsSection"> + <element name="openContextMenu" type="button" selector=".three-dots"/> + <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> + <element name="edit" type="button" selector="[data-ui-id='action-edit']"/> + <element name="imageInGrid" type="button" selector="//li[@data-ui-id='title'and text()='{{imageTitle}}']/parent::*/parent::*/parent::div//img[@class='media-gallery-image-column']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml new file mode 100644 index 0000000000000..32cd99bfe6b11 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryImageDescriptionSection"> + <element name="title" type="text" selector=".masonry-image-description .name"/> + <element name="contentType" type="text" selector=".masonry-image-description .type"/> + <element name="dimensions" type="text" selector=".masonry-image-description .dimensions" /> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml new file mode 100644 index 0000000000000..a40a70c5f160c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryMassActionSection"> + <element name="massActionCheckbox" type="button" selector="//input[@type='checkbox'][@data-ui-id ='{{imageName}}']" parameterized="true"/> + <element name="totalSelected" type="text" selector=".mediagallery-massaction-items-count > .selected_count_text"/> + <element name="cancelMassActionMode" type="button" selector="#cancel_massaction"/> + <element name="deleteSelected" type="button" selector="#delete_massaction"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml new file mode 100644 index 0000000000000..0bcbeb0d7a00f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryViewDetailsSection"> + <element name="title" type="text" selector=".image-title"/> + <element name="contentType" type="text" selector="[data-ui-id='content-type']"/> + <element name="type" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Type')]/following-sibling::div"/> + <element name="height" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Height')]/following-sibling::div"/> + <element name="description" type="text" selector=".image-details-section.description p"/> + <element name="keywords" type="text" selector="//div[@class='tags-list']"/> + <element name="filename" type="text" selector=".image-details-section.filename p"/> + <element name="edit" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'edit')]"/> + <element name="delete" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'delete')]"/> + <element name="confirmDelete" type="button" selector=".action-accept"/> + <element name="addImage" type="button" selector=".add-image-action"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml new file mode 100644 index 0000000000000..4c9e6bf362194 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMediaGalleryFolderSection"> + <element name="folderNewModalHeader" type="block" selector="//h1[contains(text(), 'New Folder Name')]"/> + <element name="folderDeleteModalHeader" type="block" selector="//h1[contains(text(), 'Are you sure you want to delete this folder?')]"/> + <element name="folderNewCreateButton" type="button" selector="#create_folder"/> + <element name="folderDeleteButton" type="button" selector="#delete_folder"/> + <element name="folderConfirmDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'OK')]"/> + <element name="folderCancelDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'Cancel')]"/> + <element name="folderNameField" type="button" selector="[name=folder_name]"/> + <element name="folderConfirmCreateButton" type="button" selector="//button/span[contains(text(),'Confirm')]"/> + <element name="folderNameValidationMessage" type="block" selector="label.mage-error"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml new file mode 100644 index 0000000000000..9271c0ff61618 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMediaGalleryHeaderButtonsSection"> + <element name="addSelected" type="button" selector=".media-gallery-add-selected"/> + <element name="deleteSelected" type="button" selector="#delete_selected"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml new file mode 100644 index 0000000000000..4749fc4a885b0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MediaGalleryUiSuite"> + <before> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYG" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="enableEnhancedMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYG" /> + </after> + <include> + <group name="media_gallery_ui"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml new file mode 100644 index 0000000000000..94831b039b53a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryDeleteImagesInBulkTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1488"/> + <title value="User deletes images with less clicks"/> + <stories value="[Story #42] User deletes images in bulk"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User deletes images with less clicks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToVerifyMode"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup" stepKey="assertMassActionModeAvailable"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup" stepKey="disableMassActionMode"/> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup" stepKey="assertImagesDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup" stepKey="assertMassectionModeDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml new file mode 100644 index 0000000000000..52f3a8079e962 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryDuplicatedImagesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1500"/> + <title value="User can filter duplicated images"/> + <stories value="[Story 59] User finds image duplicates"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User can filter duplicated images"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup" stepKey="SelectDuplicatedFilter"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertFirstImageInGrid"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertSecondImageInGrid"> + <argument name="title" value="ImageUpload_1.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml new file mode 100644 index 0000000000000..f026b87f7ec88 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryUploadImageWithMetadataTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="Magento extracts image meta data from file"/> + <stories value="Story 53 - Magento extracts image meta data from file"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="Magento extracts image meta data from file"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteJpegImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadPngImage"> + <argument name="image" value="ImageUploadPng"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewPngImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyPngImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyPngImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyPngImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deletePngImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadGifImage"> + <argument name="image" value="ImageUploadGif"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewGifImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyGifImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyGifImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyGifImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteGifImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml new file mode 100644 index 0000000000000..67bb09298893d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyAssetFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1292"/> + <title value="User sees entities where asset is used in"/> + <stories value="Story 58: User sees entities where asset is used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951024"/> + <description value="User sees entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryGridPage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml new file mode 100644 index 0000000000000..4719b98c78dbe --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1489"/> + <title value="User filters images that are not used in the content"/> + <stories value="Story 52: User filters images that are not used in the content"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images that are not used in the content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Not used anywhere"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml new file mode 100644 index 0000000000000..d54399bdeb2b2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyUsedInFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1567"/> + <title value="User filters images by the area they used in"/> + <stories value="User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images by the area they used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterIMage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Categories"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml new file mode 100644 index 0000000000000..cb7adf3307865 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryAddCategoryImageFromTwoComponentsTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User add category image via wysiwyg and image uploader button"/> + <stories value="Story [54]: User inserts image rendition to the content with text area + Insert image button" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User add category image via wysiwyg and image uploader button"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="reSaveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertContentImageIsVisible"> + <argument name="imageName" value="{{ImageUpload3.fileName}}"/> + </actionGroup> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertCategoryImageIsVisible"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml new file mode 100644 index 0000000000000..30f1412a5b08d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryAddCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1073"/> + <title value="User add category image via wysiwyg"/> + <stories value="User add category image via wysiwyg"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4484351"/> + <description value="User add category image via wysiwyg"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertImageInCategoryDescriptionField"> + <argument name="imageName" value="{{ImageUpload3.fileName}}" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml new file mode 100644 index 0000000000000..94307fa510a50 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryAddFromImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1229"/> + <stories value="[Story #38] User views basic image attributes in Media Gallery"/> + <title value="Adding image from the Image Details"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4569982"/> + <description value="Adding image from the Image Details"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup" stepKey="addImageFromViewDetails"/> + <actionGroup ref="AssertImageAddedToPageContentActionGroup" stepKey="assertImageAddedToContent"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..6e6f5240e84be --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1046; https://github.com/magento/adobe-stock-integration/issues/1047"/> + <stories value="Creating, deleting new folder functionality in Media Gallery"/> + <title value="Creating, deleting new folder functionality in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4456547; https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4457075"/> + <description value="Creating, deleting new folder functionality in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml new file mode 100644 index 0000000000000..980d6b7c85c20 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDeleteImageContextMenuTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/710"/> + <title value="Uploading and deleting an image using context menu"/> + <stories value="[Story #52] User accesses Media Gallery from the main navigation"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="Uploading and deleting an image using context menu"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml new file mode 100644 index 0000000000000..ad364e7709a33 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDeleteImageFileTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1094"/> + <title value="Deleting new image file functionality in Enhanced Media Gallery"/> + <stories value="Deleting new image file functionality in Enhanced Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4756652"/> + <description value="Deleting new image file functionality in Enhanced Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="verifyImageIsDeleted"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml new file mode 100644 index 0000000000000..6ae8ed7047434 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDeleteImageWithWarningPopupTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1511"/> + <title value="User sees warning when deleting image if it's used on storefront"/> + <stories value="User sees warning when deleting image if it's used on storefront"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4843896"/> + <description value="User sees warning when deleting image if it's used on storefront"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToAssertMessage"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup" stepKey="assertMessageImageUsedIn"> + <argument name="messageText" value="The selected assets are used in the content of the following entities: Categories(1)"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml new file mode 100644 index 0000000000000..963a0b954e45b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDisabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by disabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by disabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="disableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Disabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..960443998d010 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryEditImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml new file mode 100644 index 0000000000000..c2b167912dda7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryEnabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by enabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by enabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Enabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml new file mode 100644 index 0000000000000..e1e7bf1f0bcbb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryFilterImagesBySourceTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1393"/> + <title value="User filters images by source filter"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4760144"/> + <description value="User filters images by source filter"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteContentImage"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilterToApplySourceFilter"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyAdobeStockFilter"> + <argument name="filterValue" value="Adobe Stock"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFiltersWithAdobeStockOption"/> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml new file mode 100644 index 0000000000000..b8ce1f76ad4c8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGallerySaveFiltersStateTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1397"/> + <title value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <stories value="User is able to use bookmarks controls in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4763040"/> + <description value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySaveCustomViewActionGroup" stepKey="saveCustomView"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup" stepKey="assertFilterApplied"> + <argument name="resultValue" value="Uploaded Locally"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup" stepKey="assertNoActiveFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeleteGridViewActionGroup" stepKey="deleteView"> + <argument name="viewToDelete" value="Test View"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml new file mode 100644 index 0000000000000..eceda879e5597 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryStoreViewCategoryFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by category store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by category store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml new file mode 100644 index 0000000000000..86cae11267eaa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryStoreViewContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by content store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by content store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="createCMSPage" stepKey="deleteCmsPage"/> + </after> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSavePage"/> + <waitForPageLoad stepKey="waitForPageSave"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml new file mode 100644 index 0000000000000..ca7a71258fead --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryUploadCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1435"/> + <stories value="User uploads image outside of the Media Gallery"/> + <title value="User uploads image outside of the Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> + <description value="User uploads image outside of the Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteCategoryImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ProductImage.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml new file mode 100644 index 0000000000000..01a26cce1b6fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryVerifyImageGridAttributesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/708"/> + <title value="Verify image grid attributes"/> + <stories value="[Story #41] User views limited image information from the image grid in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/3839218"/> + <description value="User views basic image attributes in Media Gallery grid"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="assertImageAttributes"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml new file mode 100644 index 0000000000000..00fc07eb6c1af --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsDeleteImageTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/461"/> + <title value="Deleting an image from view details panel"/> + <stories value="[Story #42] User deletes images"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4516773"/> + <description value="Deleting an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageMetadata"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..92909bcf06795 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..c9447d5cc8a52 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..164ab523d508a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1119; https://github.com/magento/adobe-stock-integration/issues/1120"/> + <stories value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <title value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503041; https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503101"/> + <description value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..ede3a452e4ca5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in standalone media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..2cf6bf5dfe623 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from standalone view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from standalone view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminMediaGalleryEditAssetAddKeywordActionGroup" stepKey="setKeywords"> + <argument name="keyword" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyMetadataKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..bb7071497ce24 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details in standalone media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503223"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..3d4e523d0d6b1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Config data test. + */ +class ConfigTest extends TestCase +{ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Config + */ + private $config; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->config = $this->objectManager->getObject( + Config::class, + [ + 'scopeConfig' => $this->scopeConfigMock + ] + ); + } + + /** + * Get Magento media gallery enabled test. + */ + public function testIsEnabled(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('isSetFlag') + ->with(self::XML_PATH_ENABLED) + ->willReturn(true); + $this->assertEquals(true, $this->config->isEnabled()); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php new file mode 100644 index 0000000000000..fc8a0756a7b55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\UploadImage; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Provides test for upload image functionality + */ +class UploadImageTest extends TestCase +{ + /** + * @var Storage|MockObject + */ + private $imagesStorageMock; + + /** + * @var Filesystem|MockObject + */ + private $fileSystemMock; + + /** + * @var Read|MockObject + */ + private $mediaDirectoryMock; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->imagesStorageMock = $this->createMock(Storage::class); + $this->fileSystemMock = $this->createMock(Filesystem::class); + $this->mediaDirectoryMock = $this->createMock(Read::class); + + $this->uploadImage = (new ObjectManager($this))->getObject( + UploadImage::class, + [ + 'imagesStorage' => $this->imagesStorageMock, + 'filesystem' => $this->fileSystemMock, + ] + ); + } + + /** + * Test successful image file upload. + * + * @param string $targetFolder + * @param string|null $type + * @param string $absolutePath + * + * @dataProvider executeDataProvider + */ + public function testExecute(string $targetFolder, string $type = null, string $absolutePath): void + { + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(true); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with($targetFolder) + ->willReturn($absolutePath); + + $uploadResult = ['path' => 'media/catalog', 'file' => 'test-image.jpeg']; + $this->imagesStorageMock->expects($this->once()) + ->method('uploadFile') + ->with($absolutePath, $type) + ->willReturn($uploadResult); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Test upload image method with logical exception when the folder is not a folder. + */ + public function testExecuteWithException(): void + { + $targetFolder = 'not-a-folder'; + $type = 'image'; + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(false); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Directory not-a-folder does not exist in media directory.'); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Provides test case data. + * + * @return array + */ + public function executeDataProvider(): array + { + return [ + [ + 'targetFolder' => 'media/catalog', + 'type' => 'image', + 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg new file mode 100644 index 0000000000000..5244f8dc420e1 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg differ diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg new file mode 100644 index 0000000000000..5244f8dc420e1 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg differ diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php new file mode 100644 index 0000000000000..4047a4fcb98d8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Directories tree component + */ +class DirectoriesTree extends Container +{ + /** + * @var UrlInterface + */ + private $url; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'getDirectoryTreeUrl' => $this->url->getUrl("media_gallery/directories/gettree"), + 'deleteDirectoryUrl' => $this->url->getUrl("media_gallery/directories/delete"), + 'createDirectoryUrl' => $this->url->getUrl("media_gallery/directories/create") + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php new file mode 100644 index 0000000000000..ad5e27381dee2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\File\Size; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Image Uploader component + */ +class ImageUploader extends Container +{ + private const ACCEPT_FILE_TYPES = '/(\.|\/)(gif|jpe?g|png)$/i'; + private const ALLOWED_EXTENSIONS = 'jpg jpeg png gif'; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @var Size + */ + private $size; + + /** + * @param Size $size + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + Size $size, + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->size = $size; + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'imageUploadUrl' => $this->url->getUrl('media_gallery/image/upload', ['type' => 'image']), + 'acceptFileTypes' => self::ACCEPT_FILE_TYPES, + 'allowedExtensions' => self::ALLOWED_EXTENSIONS, + 'maxFileSize' => $this->size->getMaxFileSize() + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php new file mode 100644 index 0000000000000..1fc5a80960a69 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +/** + * Image Uploader component + */ +class ImageUploaderStandAlone extends ImageUploader +{ + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'actionsPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_columns.thumbnail_url', + 'directoriesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_directories', + 'messagesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing.messages' + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php new file mode 100644 index 0000000000000..aec99bf7d3b8c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Asset\Repository as AssetRepository; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Source icon url provider + */ +class SourceIconProvider extends Column +{ + /** + * @var array + */ + private $sourceIcons; + + /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param AssetRepository $assetRepository + * @param ScopeConfigInterface $scopeConfig + * @param array $components + * @param array $data + * @param array $sourceIcons + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + AssetRepository $assetRepository, + ScopeConfigInterface $scopeConfig, + array $components = [], + array $data = [], + array $sourceIcons = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->assetRepository = $assetRepository; + $this->scopeConfig = $scopeConfig; + $this->sourceIcons = $sourceIcons; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items']) && is_iterable($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')] = $item[$this->getData('name')] + ? $this->getSourceIconUrl($item[$this->getData('name')]) + : null; + } + } + + return $dataSource; + } + + /** + * Construct source icon url based on the source code matching + * + * @param string $sourceName + * + * @return string|null + */ + public function getSourceIconUrl(string $sourceName): ?string + { + return isset($this->sourceIcons[$sourceName]) + ? $this->assetRepository->getUrlWithParams( + $this->sourceIcons[$sourceName], + ['_secure' => $this->isSecure()] + ) + : null; + } + + /** + * Check if store use secure connection + * + * @return bool + */ + private function isSecure(): bool + { + return $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php new file mode 100644 index 0000000000000..481f8ab861f0f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Backend\Model\UrlInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Overlay column + */ +class Url extends Column +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * UrlInterface $urlInterface + */ + private $urlInterface; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param UrlInterface $urlInterface + * @param Images $images + * @param Storage $storage + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + UrlInterface $urlInterface, + Images $images, + Storage $storage, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->storeManager = $storeManager; + $this->urlInterface = $urlInterface; + $this->images = $images; + $this->storage = $storage; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + * @throws NoSuchEntityException + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + $item['encoded_id'] = $this->images->idEncode($item['path']); + $item[$this->getData('name')] = $this->getUrl($item[$this->getData('name')]); + } + } + + return $dataSource; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'), + 'storeId' => $this->storeManager->getStore()->getId() + ] + ) + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws NoSuchEntityException + */ + private function getUrl(string $path): string + { + return $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $path); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php new file mode 100644 index 0000000000000..273cf9e37554b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; + +/** + * Asset filter + */ +class Asset extends Select +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param GetContentByAssetIdsInterface $getContentIdentities + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + GetContentByAssetIdsInterface $getContentIdentities, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->getContentIdentities = $getContentIdentities; + } + + /** + * Apply filter + * + * @return void + */ + public function applyFilter() + { + if (isset($this->filterData[$this->getName()])) { + $ids = is_array($this->filterData[$this->getName()]) + ? $this->filterData[$this->getName()] + : [$this->filterData[$this->getName()]]; + $filter = $this->filterBuilder->setConditionType('in') + ->setField($this->_data['config']['identityColumn']) + ->setValue($this->getEntityIdsByAsset($ids)) + ->create(); + + $this->getContext()->getDataProvider()->addFilter($filter); + } + } + + /** + * Return entity ids by assets ids. + * + * @param array $ids + */ + private function getEntityIdsByAsset(array $ids): string + { + if (!empty($ids)) { + $categoryIds = []; + $data = $this->getContentIdentities->execute($ids); + foreach ($data as $identity) { + if ($identity->getEntityType() === $this->_data['config']['entityType']) { + $categoryIds[] = $identity->getEntityId(); + } + } + return implode(',', $categoryIds); + } + return ''; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php new file mode 100644 index 0000000000000..31c658a6c4208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Status filter options + */ +class Status implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + ['value' => '1', 'label' => __('Enabled')], + ['value' => '0', 'label' => __('Disabled')] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php new file mode 100644 index 0000000000000..cf49377c19837 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; + +/** + * Store Options for content field + */ +class Store extends StoreOptions +{ + /** + * All Store Views value + */ + const ALL_STORE_VIEWS = '0'; + + /** + * Get options + * + * @return array + */ + public function toOptionArray() + { + if ($this->options !== null) { + return $this->options; + } + + $this->currentOptions['All Store Views']['label'] = __('All Store Views'); + $this->currentOptions['All Store Views']['value'] = self::ALL_STORE_VIEWS; + + $this->generateCurrentOptions(); + + $this->options = array_values($this->currentOptions); + + return $this->options; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php new file mode 100644 index 0000000000000..d3f758a510888 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Used in filter options + */ +class UsedIn implements OptionSourceInterface +{ + /** + * @var array + */ + private $options; + + /** + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + } + + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return $this->options; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php new file mode 100644 index 0000000000000..160097967165d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\MediaGalleryUi\Ui\Component\Listing; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Psr\Log\LoggerInterface as Logger; + +class Provider extends SearchResult +{ + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param GetAssetsKeywordsInterface $getAssetKeywords + * @param string $mainTable + * @param null|string $resourceModel + * @param null|string $identifierName + * @param null|string $connectionName + * @throws LocalizedException + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + GetAssetsKeywordsInterface $getAssetKeywords, + $mainTable = 'media_gallery_asset', + $resourceModel = null, + $identifierName = null, + $connectionName = null + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName, + $connectionName + ); + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * @inheritdoc + */ + public function getData() + { + $data = parent::getData(); + $keywords = []; + foreach ($this->_items as $asset) { + $keywords[$asset->getId()] = array_map(function (AssetKeywordsInterface $assetKeywords) { + return array_map(function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, $assetKeywords->getKeywords()); + }, $this->getAssetKeywords->execute([$asset->getId()])); + } + + /** @var AssetInterface $asset */ + foreach ($data as $key => $asset) { + $data[$key]['thumbnail_url'] = $asset['path']; + $data[$key]['content_type'] = strtoupper(str_replace('image/', '', $asset['content_type'])); + $data[$key]['preview_url'] = $asset['path']; + $data[$key]['keywords'] = isset($keywords[$asset['id']]) ? implode(",", $keywords[$asset['id']]) : ''; + $data[$key]['source'] = empty($asset['source']) ? __('Local') : $asset['source']; + } + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json new file mode 100644 index 0000000000000..f4701306eb369 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-media-gallery-ui", + "description": "Magento module responsible for the media gallery UI implementation", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/module-store": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..bf07512d13d4f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField"> + <arguments> + <argument name="getAssetIdsByContentStatus" xsi:type="object">Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="path" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Directory</item> + <item name="fulltext" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Keyword</item> + <item name="entity_type" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\EntityType</item> + <item name="duplicated" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Duplicated</item> + <item name="content_status" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + <item name="store_id" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\SortingProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="filters" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor</item> + <item name="sorting" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor</item> + <item name="pagination" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor</item> + <item name="joins" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\Listing\DataProvider"> + <arguments> + <argument name="collectionProcessor" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"> + <arguments> + <argument name="options" xsi:type="array"> + <item name="cms_page" xsi:type="array"> + <item name="value" xsi:type="string">cms_page</item> + <item name="label" xsi:type="string" translate="true">Pages</item> + </item> + <item name="catalog_category" xsi:type="array"> + <item name="value" xsi:type="string">catalog_category</item> + <item name="label" xsi:type="string" translate="true">Categories</item> + </item> + <item name="cms_block" xsi:type="array"> + <item name="value" xsi:type="string">cms_block</item> + <item name="label" xsi:type="string" translate="true">Blocks</item> + </item> + <item name="catalog_product" xsi:type="array"> + <item name="value" xsi:type="string">catalog_product</item> + <item name="label" xsi:type="string" translate="true">Products</item> + </item> + <item name="not_used" xsi:type="array"> + <item name="value" xsi:type="string">not_used</item> + <item name="label" xsi:type="string" translate="true">Not used anywhere</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..92839aa75ac8b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_Cms::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> + <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_Cms::media_gallery"/> + </menu> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..11a555e16e957 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery" frontName="media_gallery"> + <module name="Magento_MediaGalleryUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..77544b42e899a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enhanced Media Gallery</label> + <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>system/media_gallery/enabled</config_path> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/config.xml b/app/code/Magento/MediaGalleryUi/etc/config.xml new file mode 100644 index 0000000000000..fe8e73c406e59 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/config.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery> + <enabled>0</enabled> + </media_gallery> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml new file mode 100644 index 0000000000000..040e003817efa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryUiApi\Api\ConfigInterface" type="Magento\MediaGalleryUi\Model\Config"/> + <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> + <arguments> + <argument name="collections" xsi:type="array"> + <item name="media_gallery_listing_data_source" xsi:type="string">Magento\MediaGalleryUi\Ui\Component\Listing\Provider</item> + </argument> + </arguments> + </type> + <virtualType name="mediaGallerySearchResult" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult"> + <arguments> + <argument name="mainTable" xsi:type="string">media_gallery_asset_grid</argument> + <argument name="resourceModel" xsi:type="string">Magento\MediaGalleryUi\Model\ResourceModel\Grid\Asset</argument> + </arguments> + </virtualType> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="resizeParameters" xsi:type="array"> + <item name="height" xsi:type="number">200</item> + <item name="width" xsi:type="number">200</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Model\Directories\FolderTree"> + <arguments> + <argument name="path" xsi:type="string">media</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\Type"> + <arguments> + <argument name="types" xsi:type="array"> + <item name="image" xsi:type="string">Image</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <plugin name="createMediaGalleryThumbnails" type="Magento\MediaGalleryUi\Plugin\CreateThumbnails"/> + </type> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProviderPool"> + <arguments> + <argument name="detailsProviders" xsi:type="array"> + <item name="10" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Type</item> + <item name="20" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\CreatedAt</item> + <item name="30" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\UpdatedAt</item> + <item name="40" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Width</item> + <item name="50" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Height</item> + <item name="60" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Size</item> + <item name="70" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/module.xml b/app/code/Magento/MediaGalleryUi/etc/module.xml new file mode 100644 index 0000000000000..0deede3e6aad0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryUi"> + <sequence> + <module name="Magento_Cms" /> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryUi/i18n/en_US.csv b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv new file mode 100644 index 0000000000000..1882665ce8033 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv @@ -0,0 +1,8 @@ +"Enhanced Media Gallery","Enhanced Media Gallery" +Enabled,Enabled +All,All +Directory,Directory +"Uploaded Date","Uploaded Date" +"Modification Date","Modification Date" +Overlay,Overlay +"Thumbnail Image","Thumbnail Image" diff --git a/app/code/Magento/MediaGalleryUi/registration.php b/app/code/Magento/MediaGalleryUi/registration.php new file mode 100644 index 0000000000000..e1d321c5a8ff3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUi', __DIR__); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml new file mode 100644 index 0000000000000..f41c0f91b2249 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <container name="root"> + <block name="media.gallery.container" + class="Magento\Backend\Block\Template" + template="Magento_MediaGalleryUi::container.phtml" + aclResource="Magento_Cms::media_gallery"> + <container name="gallery.actions" htmlTag="div" htmlClass="page-main-actions"> + <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> + </container> + <uiComponent name="media_gallery_listing"/> + <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details.phtml"> + <arguments> + <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + </arguments> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </block> + </container> +</layout> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml new file mode 100644 index 0000000000000..7750f22b39ce7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> + <uiComponent name="standalone_media_gallery_listing"/> + <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <arguments> + <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + </arguments> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details_standalone.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml new file mode 100644 index 0000000000000..5b905ea97d64a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength + +?> + +<div class="media-gallery-container"> + <?= $block->getChildHtml(); ?> +</div> + +<script type="text/x-magento-init"> + { + ".media-gallery-container": { + "Magento_Ui/js/core/app": { + "components": { + "media_gallery_container": { + "component": "Magento_MediaGalleryUi/js/container", + "containerSelector": ".media-gallery-container", + "masonryComponentPath": "media_gallery_listing.media_gallery_listing.media_gallery_columns" + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml new file mode 100644 index 0000000000000..ba2033478afa1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; +use Magento\Framework\Escaper; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var Escaper $escaper */ + +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-image-actions" + data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "media_gallery_listing.media_gallery_listing.messages" + } + } + } + }, + "#media-gallery-image-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", + "handler": "deleteImageAction", + "name": "delete", + "classes": "action-default scalable delete action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", + "handler": "addImage", + "name": "add-image", + "classes": "scalable action-primary add-image-action" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml new file mode 100644 index 0000000000000..9fc0e749ac888 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-image-actions" + data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + } + }, + "#media-gallery-image-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", + "handler": "deleteImageAction", + "name": "delete", + "classes": "action-default scalable delete action-quaternary" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml new file mode 100644 index 0000000000000..c2b7e66cc89bd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-edit-image-actions" + data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + }, + "#media-gallery-image-edit-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-edit-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml new file mode 100644 index 0000000000000..ec48ed8bb9053 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-edit-image-actions" + data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + }, + "#media-gallery-image-edit-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-edit-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml new file mode 100644 index 0000000000000..86c8590bb4860 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">cms_block</item> + <item name="identityColumn" xsi:type="string">block_id</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml new file mode 100644 index 0000000000000..58881a8c9de6c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">cms_page</item> + <item name="identityColumn" xsi:type="string">page_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..5a16ed1792159 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,393 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <buttons> + <button name="add_selected"> + <param name="on_click" xsi:type="string">return false;</param> + <param name="sort_order" xsi:type="number">110</param> + <class>action-primary no-display media-gallery-add-selected</class> + <label translate="true">Add Selected</label> + </button> + <button name="cancel"> + <param name="on_click" xsi:type="string">MediabrowserUtility.closeDialog();</param> + <param name="sort_order" xsi:type="number">1</param> + <class>cancel action-quaternary</class> + <label translate="true">Cancel</label> + </button> + <button name="upload_image"> + <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> + <class>action-add scalable media-gallery-actions-buttons</class> + <param name="sort_order" xsi:type="number">20</param> + <label translate="true">Upload Image</label> + </button> + <button name="delete_folder"> + <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> + <param name="disabled" xsi:type="string">disabled</param> + <param name="sort_order" xsi:type="number">30</param> + <class>action-default scalable media-gallery-actions-buttons</class> + <label translate="true">Delete Folder</label> + </button> + <button name="create_folder"> + <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> + <param name="sort_order" xsi:type="number">10</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Create Folder</label> + </button> + <button name="delete_massaction"> + <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> + <param name="sort_order" xsi:type="number">50</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Images...</label> + </button> + </buttons> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + media_gallery_listing.media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <container name="media_gallery_massactions" + displayArea="sorting" + sortOrder="10" + component="Magento_MediaGalleryUi/js/grid/massaction/massactions" + template="Magento_MediaGalleryUi/grid/massactions/count" > + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + template="Magento_MediaGalleryUi/grid/directories/directoryTree" + component="Magento_MediaGalleryUi/js/directory/directoryTree"/> + <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">media_gallery_listing.media_gallery_listing.messages</item> + <item name="imageModelname" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploader" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml new file mode 100644 index 0000000000000..2b7d9fde3b9ff --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">catalog_product</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..c96ad0fd86661 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,380 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + standalone_media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + <buttons> + <button name="delete_folder"> + <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> + <param name="disabled" xsi:type="string">disabled</param> + <param name="sort_order" xsi:type="number">20</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Folder</label> + </button> + <button name="create_folder"> + <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> + <param name="sort_order" xsi:type="number">30</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Create Folder</label> + </button> + <button name="delete_massaction"> + <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> + <param name="sort_order" xsi:type="number">50</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Images...</label> + </button> + <button name="upload_image"> + <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Upload Image</label> + </button> + </buttons> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="standalone_media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <container name="media_gallery_massactions" + displayArea="sorting" + sortOrder="10" + component="Magento_MediaGalleryUi/js/grid/massaction/massactions" + template="Magento_MediaGalleryUi/grid/massactions/count" > + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">standalone_media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + template="Magento_MediaGalleryUi/grid/directories/directoryTree" + component="Magento_MediaGalleryUi/js/directory/directoryTree"/> + <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="url" xsi:type="string">thumbnail_url</item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.messages</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploaderStandAlone" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..671a82dce3f58 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,478 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// +// Variables +// _____________________________________________ + +@color-folders-background: #a6a6a6; +@color-folders-background-selected: #cdecf6; +@color-folders-border: #7185f5; +@color-masonry-overlay: #d9631c; +@color-masonry-grey: #9e9e9e; +@color-masonry-white: #e1e1e1; +@color-masonry-steelblue: #4682b4; +@color-media-gallery-buttons-background: #e3e3e3; +@color-media-gallery-buttons-border: #adadad; +@color-media-gallery-buttons-text: #514943; +@color-media-gallery-checkbox-background: #eee; + +& when (@media-common = true) { + + .media-gallery-delete-image-action, + .delete-folder-confirmation-popup { + + .modal-content { + word-wrap: anywhere; + } + } + + .media-gallery-asset-ui-select-filter { + + .admin__action-multiselect-crumb { + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis + } + + .admin__action-multiselect-label > span { + display: block; + margin-top: -2px; + max-height: 18px; + max-width: 70%; + overflow: hidden; + padding-left: 23px; + position: absolute; + text-overflow: ellipsis; + } + + .admin__action-multiselect-item-path { + float: right; + max-height: 70px; + max-width: 70px; + } + + .admin__action-multiselect-label { + display: inline-block; + width: 100%; + } + } + + .page-actions-buttons > button.no-display { + display: none; + } + + .page-actions-buttons > button.media-gallery-actions-buttons, + .page-actions .page-actions-buttons > button.media-gallery-actions-buttons:focus, + .page-actions-buttons > button.media-gallery-actions-buttons:hover { + background-color: @color-media-gallery-buttons-background; + border-color: @color-media-gallery-buttons-border; + color: @color-media-gallery-buttons-text; + } + + .mediagallery-massaction-checkbox { + background-color: @color-media-gallery-checkbox-background; + border-radius: 4px; + height: 40px; + input[type='checkbox'] { + margin-left: 10px; + margin-top: 11px; + } + margin-left: 15px; + margin-top: 10px; + position: absolute; + width: 40px; + z-index: 10; + } + + .mediagallery-massaction-items-count { + display: inline-block; + margin-left: -15px; + padding-right: 20px; + } + + .media-gallery-container { + + .masonry-image-grid .no-data-message-container, + .masonry-image-grid .error-message-container { + left: 50%; + margin-right: -50%; + position: sticky; + top: 50%; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + + .page-main-actions { + .page-actions { + .media-gallery-add-selected { + order: unset; + } + } + + & > .page-actions { + & > button.no-display { + display: none; + } + } + } + .jstree-default .jstree-hovered { + background: @color-folders-background; + border-color: @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .jstree-default .jstree-leaf a .jstree-icon { + background-position: -52px -16px; + } + + + .jstree-default a .jstree-icon { + background-position: -52px -16px; + } + + .jstree-default .jstree-no-dots .jstree-open > a > ins { + background-position: -52px -38px; + height: 20px; + width: 29px; + } + + .jstree a > ins { + float: left; + height: 22px; + margin-top: -3px; + width: 20px; + } + + .jstree-default .jstree-no-dots .jstree-leaf > ins { + background-image: none; + } + + .jstree-default ins { + background-image: url("@{baseDir}Magento_MediaGalleryUi/images/d.png"); + } + + .jstree a { + height: 30px; + margin: 1px; + padding-left: 6px; + padding-top: 6px; + width: 100%; + } + + .jstree-default .jstree-clicked { + background: @color-folders-background-selected; + border: .14em solid @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .masonry-image-overlay { + background-color: @color-masonry-overlay; + float: right; + font-size: 11px; + margin-left: 120px; + margin-top: 170px; + padding: .3rem; + pointer-events: none; + position: relative; + } + + .media-gallery-image-details { + float: left; + list-style: none; + margin-bottom: 0; + position: absolute; + width: 89%; + + .name { + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + display: -webkit-box; + font-size: 15px; + font-weight: bold; + line-height: 20px; + max-height: 50px; + overflow: hidden; + padding-bottom: 2px; + text-overflow: ellipsis; + white-space: pre-line; + word-wrap: anywhere; + word-wrap: break-word; + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + white-space: nowrap; + } + } + + .type { + display: inline-block; + font-size: 12px; + padding-bottom: 5px; + } + + .dimensions { + display: inline-block; + } + + .source { + display: inline-block; + } + } + + .media-gallery-image-actions { + float: right; + position: absolute; + right: 0; + width: 10%; + + .action-select-wrap { + cursor: pointer; + } + + .three-dots { + &:before { + content: url("@{baseDir}Magento_MediaGalleryUi/images/3-dots.png"); + cursor: pointer; + } + } + } + + .media-gallery-image { + height: 200px; + margin: 0 auto; + position: relative; + text-align: center; + width: 200px; + } + + .masonry-image-description { + background-color: @color-white; + min-height: 90px; + padding-top: 10px; + position: relative; + } + + .masonry-image-column { + background-color: @color-masonry-white; + width: 200px; + } + + .media-directory-container { + float: left; + padding-right: 40px; + } + + .media-gallery-image-block { + cursor: pointer; + height: 200px; + margin: 0 auto; + position: relative; + + &.selected { + border: 5px solid @color-masonry-steelblue; + } + } + + .media-gallery-image { + img { + bottom: 0; + height: auto; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + padding: 5px; + position: absolute; + right: 0; + top: 0; + width: auto; + } + + .action-menu { + bottom: 0; + float: right; + left: auto; + top: auto; + z-index: 100; + } + } + + .adobe-stock-icon { + margin-bottom: -6px; + width: 29px; + } + + .masonry-image-grid { + align-items: first baseline; + display: grid; + grid-template-columns: repeat(auto-fill, 210px); + justify-content: end; + margin: 10px 0; + position: relative; + } + + .admin__data-grid-filters .admin__form-field { + .action-select-wrap { + .action-menu { + width: 110%; + } + .admin__action-multiselect-search-label { + right: 1.5rem; + } + } + + .action-close { + padding: 0; + &:before { + font-size: 6px; + } + } + } + } + + .media-gallery-image-details-modal, + .media-gallery-edit-image-details-modal { + + .admin__action-multiselect-crumb { + .action-close { + padding: 0; + + &:before { + font-size: .5em; + } + } + } + + .edit-image-details { + padding: 50px; + } + + .path-display { + margin-top: 8px; + } + + .page-action-buttons { + float: right; + } + + .image-type { + .adobe-stock-icon { + margin-bottom: -6px; + width: 29px; + } + + .type { + color: @color-very-dark-gray; + } + } + + .image-details { + .lib-vendor-prefix-display(); + + .image-details-image { + img { + max-height: 650px; + } + } + + .image-details-sidebar { + .lib-vendor-prefix-flex-grow(1); + margin-top: 0; + padding-left: 40px; + + .image-details-section { + margin-bottom: 40px; + max-width: 400px; + min-width: 290px; + word-wrap: anywhere; + .lib-clearfix(); + } + + h3.image-title { + font-weight: bold; + line-height: 1.5; + } + + .attributes { + .attribute { + &:not(:last-child) { + margin-bottom: 20px; + padding-bottom: 20px; + } + + & > * { + float: left; + width: 50%; + } + + .value { + display: inline; + float: right; + } + + .title { + color: @color-very-dark-gray; + } + } + } + + .tags { + .tags-list { + margin-bottom: 10px; + + .show-more-item { + display: none; + } + + &.show-all-tags { + margin-bottom: 0; + + .show-more-item { + display: inline; + } + + & + .show-more-link-container { + display: none; + } + } + } + } + } + } + } + .masonry-image-sortby { + display: inline-block; + } + + .masonry-results-number { + display: inline-block; + margin-right: 1.4rem; + } +} + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .media-gallery-image-details-modal { + .image-details { + display: block; + + .image-details-sidebar { + margin-top: 20px; + padding-left: 0; + } + + .image-details-image img { + max-height: 450px; + } + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png new file mode 100644 index 0000000000000..601ba415f2446 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png new file mode 100644 index 0000000000000..db5cda9c5512b Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png new file mode 100644 index 0000000000000..6516e915624c3 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js new file mode 100644 index 0000000000000..51ba2a258faf1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -0,0 +1,75 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'Magento_MediaGalleryUi/js/action/deleteImages', + 'mage/translate' +], function ($, _, getDetails, deleteImages, $t) { + 'use strict'; + + return { + + /** + * Get information about image use + * + * @param {Array} recordsIds + * @param {String} imageDetailsUrl + * @param {String} deleteImageUrl + */ + deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { + var imagesCount = Object.keys(recordsIds).length, + confirmationContent = $t('%1 Are you sure you want to delete "%2" image%3?') + .replace('%2', Object.keys(recordsIds).length).replace('%3', imagesCount > 1 ? 's' : ''), + deferred = $.Deferred(); + + getDetails(imageDetailsUrl, recordsIds) + .then(function (imageDetails) { + confirmationContent = confirmationContent.replace( + '%1', + this.getRecordRelatedContentMessage(imageDetails) + ); + }.bind(this)).fail(function () { + confirmationContent = confirmationContent.replace('%1', ''); + }).always(function () { + deleteImages(recordsIds, deleteImageUrl, confirmationContent).then(function (status) { + deferred.resolve(status); + }).fail(function (error) { + deferred.reject(error); + }); + }); + + return deferred.promise(); + }, + + /** + * Get information about image use + * + * @param {Object|String} images + * @return {String} + */ + getRecordRelatedContentMessage: function (images) { + var usedInMessage = $t('The selected assets are used in the content of the following entities: '), + usedIn = []; + + $.each(images, function (key, image) { + $.each(image.details, function (sectionIndex, section) { + if (section.title === 'Used In' && _.isObject(section) && !_.isEmpty(section.value)) { + $.each(section.value, function (entityTypeIndex, entityTypeData) { + usedIn.push(entityTypeData.name + '(' + entityTypeData.number + ')'); + }); + } + }); + }); + + if (_.isEmpty(usedIn)) { + return ''; + } + + return usedInMessage + usedIn.join(', ') + '.'; + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js new file mode 100644 index 0000000000000..c8ddeaf3d3929 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'mage/url', + 'Magento_MediaGalleryUi/js/grid/messages', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function ($, _, urlBuilder, messages, confirmation, $t) { + 'use strict'; + + return function (ids, deleteUrl, confirmationContent) { + var deferred = $.Deferred(), + title = $t('Delete assets'), + cancelText = $t('Cancel'), + deleteImageText = $t('Delete'); + + /** + * Send deletion request with redords ids + * + * @param {Array} recordIds + * @param {String} serviceUrl + */ + function sendRequest(recordIds, serviceUrl) { + + $.ajax({ + type: 'POST', + url: serviceUrl, + dataType: 'json', + showLoader: true, + data: { + 'form_key': window.FORM_KEY, + 'ids': recordIds + }, + context: this, + + /** + * Success handler for deleting image + * + * @param {Object} response + */ + success: function (response) { + var message = !_.isUndefined(response.message) ? response.message : null; + + if (!response.success) { + message = message || $t('There was an error on attempt to delete the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + + deferred.reject(message); + } + + message = message || $t('You have successfully removed the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: true, + message: message, + code: 'success' + }); + deferred.resolve(message); + }, + + /** + * Error handler for deleting image + * + * @param {Object} response + */ + error: function (response) { + var message; + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('There was an error on attempt to delete the image.'); + } else { + message = response.responseJSON.message; + } + + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + deferred.reject(message); + } + }); + } + + confirmation({ + title: title, + modalClass: 'media-gallery-delete-image-action', + content: confirmationContent, + buttons: [ + { + text: cancelText, + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + deferred.resolve({ + status: 'canceled' + }); + } + }, + { + text: deleteImageText, + class: 'action-primary action-accept', + + /** + * Delete Image and close modal + */ + click: function () { + sendRequest(ids, deleteUrl); + this.closeModal(); + } + } + ] + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js new file mode 100644 index 0000000000000..ec750afff29bf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (imageDetailsUrl, imageIds) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'GET', + url: imageDetailsUrl, + dataType: 'json', + showLoader: true, + data: { + 'ids': imageIds + }, + context: this, + + /** + * Resolve with image details if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.imageDetails); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not retrieve image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js new file mode 100644 index 0000000000000..4d1120badeca0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js @@ -0,0 +1,56 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (saveImageDetailsUrl, data) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: saveImageDetailsUrl, + dataType: 'json', + showLoader: true, + data: data, + + /** + * Resolve with image details if success, reject with response message otherwise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not save image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js new file mode 100644 index 0000000000000..f6dd277fb85f5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'jquery' +], function (Element, $) { + 'use strict'; + + return Element.extend({ + defaults: { + containerSelector: '.media-gallery-container', + masonryComponentPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns', + modules: { + masonry: '${ $.masonryComponentPath }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super(); + + $(this.containerSelector).applyBindings(); + + return this; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js new file mode 100644 index 0000000000000..cc4d759069c67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js @@ -0,0 +1,61 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (createFolderUrl, paths) { + var deferred = $.Deferred(), + message, + data = { + paths: paths + }; + + $.ajax({ + type: 'POST', + url: createFolderUrl, + dataType: 'json', + showLoader: true, + data: data, + context: this, + + /** + * Resolve if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not create the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js new file mode 100644 index 0000000000000..06277481e1142 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (deleteFolderUrl, path) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: deleteFolderUrl, + dataType: 'json', + showLoader: true, + data: { + path: path + }, + context: this, + + /** + * Resolve if delete folder success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not delete the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js new file mode 100644 index 0000000000000..d7f756d8bbd90 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -0,0 +1,186 @@ +/** + * Copyright © Magento, Inc. All rights reserved.g + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/modal/alert', + 'underscore', + 'Magento_Ui/js/modal/prompt', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'Magento_MediaGalleryUi/js/directory/actions/deleteDirectory', + 'mage/translate', + 'validation' +], function ($, Component, confirm, uiAlert, _, prompt, createDirectory, deleteDirectory, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + directoryTreeSelector: '#media-gallery-directory-tree', + deleteButtonSelector: '#delete_folder', + createFolderButtonSelector: '#create_folder', + messageDelay: 5, + messagesName: 'media_gallery_listing.media_gallery_listing.messages', + modules: { + directoryTree: '${ $.parentName }.media_gallery_directories', + messages: '${ $.messagesName }' + } + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['selectedFolder']); + this.initEvents(); + + return this; + }, + + /** + * Initialize directories events + */ + initEvents: function () { + $(this.deleteButtonSelector).on('delete_folder', function () { + this.getConfirmationPopupDeleteFolder(); + }.bind(this)); + + $(this.createFolderButtonSelector).on('create_folder', function () { + this.getPrompt({ + title: $t('New Folder Name:'), + content: '', + actions: { + /** + * Confirm action + */ + confirm: function (folderName) { + createDirectory( + this.directoryTree().createDirectoryUrl, + [this.getNewFolderPath(folderName)] + ).then(function () { + this.directoryTree().reloadJsTree().then(function () { + $(this.directoryTree().directoryTreeSelector).on('loaded.jstree', function () { + this.directoryTree().locateNode(this.getNewFolderPath(folderName)); + }.bind(this)); + }.bind(this)); + + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + }, + buttons: [{ + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + } + }, { + text: $t('Confirm'), + class: 'action-primary action-accept' + }] + }); + }.bind(this)); + }, + + /** + * Return configured path for folder creation. + * + * @param {String} folderName + * @returns {String} + */ + getNewFolderPath: function (folderName) { + var selectedFolder = _.isUndefined(this.selectedFolder()) || + _.isNull(this.selectedFolder()) ? '/' : this.selectedFolder(), + folderToCreate = selectedFolder !== '/' ? selectedFolder + '/' + folderName : folderName; + + return folderToCreate; + }, + + /** + * Return configured prompt with input field + */ + getPrompt: function (data) { + prompt({ + title: $t(data.title), + content: $t(data.content), + modalClass: 'media-gallery-folder-prompt', + validation: true, + validationRules: ['required-entry', 'validate-alphanum'], + attributesField: { + name: 'folder_name', + 'data-validate': '{required:true, validate-alphanum}', + maxlength: '128' + }, + attributesForm: { + novalidate: 'novalidate', + action: '' + }, + context: this, + actions: data.actions, + buttons: data.buttons + }); + }, + + /** + * Confirmation popup for delete folder action. + */ + getConfirmationPopupDeleteFolder: function () { + confirm({ + title: $t('Are you sure you want to delete this folder?'), + modalClass: 'delete-folder-confirmation-popup', + content: $t('The following folder is going to be deleted: %1') + .replace('%1', this.selectedFolder()), + actions: { + + /** + * Delete folder on button click + */ + confirm: function () { + deleteDirectory( + this.directoryTree().deleteDirectoryUrl, + this.selectedFolder() + ).then(function () { + this.directoryTree().removeNode(); + this.directoryTree().selectStorageRoot(); + $(window).trigger('folderDeleted.enhancedMediaGallery'); + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + } + }); + }, + + /** + * Set inactive all nodes, adds disable state to Delete Folder Button + */ + setInActive: function () { + this.selectedFolder(null); + $(this.deleteButtonSelector).attr('disabled', true).addClass('disabled'); + }, + + /** + * Set active node, remove disable state from Delete Forlder button + * + * @param {String} folderId + */ + setActive: function (folderId) { + this.selectedFolder(folderId); + $(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled'); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js new file mode 100644 index 0000000000000..decc337e1b83c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -0,0 +1,477 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global Base64 */ +define([ + 'jquery', + 'uiComponent', + 'uiLayout', + 'underscore', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'jquery/jstree/jquery.jstree', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, Component, layout, _, createDirectory) { + 'use strict'; + + return Component.extend({ + defaults: { + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + directoryTreeSelector: '#media-gallery-directory-tree', + getDirectoryTreeUrl: 'media_gallery/directories/gettree', + jsTreeReloaded: null, + modules: { + directories: '${ $.name }_directories', + filterChips: '${ $.filterChipsProvider }' + }, + listens: { + '${ $.provider }:params.filters.path': 'clearFiltersHandle' + }, + viewConfig: [{ + component: 'Magento_MediaGalleryUi/js/directory/directories', + name: '${ $.name }_directories' + }] + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['activeNode']).initView(); + + $.async( + this.directoryTreeSelector, + this, + function () { + this.renderDirectoryTree().then(function () { + this.initEvents(); + }.bind(this)); + }.bind(this)); + + return this; + }, + + /** + * Render directory tree component. + */ + renderDirectoryTree: function () { + + return this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isFolderCreated) { + if (isFolderCreated) { + this.getJsonTree().then(function (newData) { + this.createTree(newData); + }.bind(this)); + } else { + this.createTree(data); + } + }.bind(this)); + }.bind(this)); + }, + + /** + * Set jstree reloaded + * + * @param {Boolean} value + */ + setJsTreeReloaded: function (value) { + this.jsTreeReloaded = value; + }, + + /** + * Create folder by provided current_tree_path param + * + * @param {Array} directories + */ + createFolderIfNotExists: function (directories) { + var isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath = isMediaBrowser ? window.MediabrowserUtility.pathId : null, + deferred = $.Deferred(), + decodedPath, + pathArray; + + if (currentTreePath) { + decodedPath = Base64.idDecode(currentTreePath); + + if (!this.isDirectoryExist(directories[0], decodedPath)) { + pathArray = this.convertPathToPathsArray(decodedPath); + + $.each(pathArray, function (i, val) { + if (this.isDirectoryExist(directories[0], val)) { + pathArray.splice(i, 1); + } + }.bind(this)); + + createDirectory( + this.createDirectoryUrl, + pathArray + ).then(function () { + deferred.resolve(true); + }); + } else { + deferred.resolve(false); + } + } else { + deferred.resolve(false); + } + + return deferred.promise(); + }, + + /** + * Verify if directory exists in array + * + * @param {Array} directories + * @param {String} directoryId + */ + isDirectoryExist: function (directories, directoryId) { + var found = false; + + /** + * Recursive search in array + * + * @param {Array} data + * @param {String} id + */ + function recurse(data, id) { + var i; + + for (i = 0; i < data.length; i++) { + if (data[i].attr.id === id) { + found = data[i]; + break; + } else if (data[i].children && data[i].children.length) { + recurse(data[i].children, id); + } + } + } + + recurse(directories, directoryId); + + return found; + }, + + /** + * Convert path string to path array e.g 'path1/path2' -> ['path1', 'path1/path2'] + * + * @param {String} path + */ + convertPathToPathsArray: function (path) { + var pathsArray = [], + pathString = '', + paths = path.split('/'); + + $.each(paths, function (i, val) { + pathString += i >= 1 ? val : val + '/'; + pathsArray.push(i >= 1 ? pathString : val); + }); + + return pathsArray; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Wait for condition then call provided callback + */ + waitForCondition: function (condition, callback) { + if (condition()) { + setTimeout(function () { + this.waitForCondition(condition, callback); + }.bind(this), 100); + } else { + callback(); + } + }, + + /** + * Remove ability to multiple select on nodes + */ + overrideMultiselectBehavior: function () { + $.jstree.defaults.ui['select_range_modifier'] = false; + $.jstree.defaults.ui['select_multiple_modifier'] = false; + }, + + /** + * Handle jstree events + */ + initEvents: function () { + this.firejsTreeEvents(); + this.overrideMultiselectBehavior(); + + $(window).on('reload.MediaGallery', function () { + this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isCreated) { + if (isCreated) { + this.renderDirectoryTree().then(function () { + this.setJsTreeReloaded(true); + this.firejsTreeEvents(); + }.bind(this)); + } else { + this.checkChipFiltersState(); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + /** + * Fire event for jstree component + */ + firejsTreeEvents: function () { + $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { + var path = $(data.rslt.obj).data('path'); + + this.setActiveNodeFilter(path); + this.setJsTreeReloaded(false); + }.bind(this)); + + $(this.directoryTreeSelector).on('loaded.jstree', function () { + this.checkChipFiltersState(); + }.bind(this)); + + }, + + /** + * Verify directory filter on init event, select folder per directory filter state + */ + checkChipFiltersState: function () { + var currentFilterPath = this.filterChips().filters.path, + isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath; + + currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : + Base64.idDecode(window.MediabrowserUtility.pathId); + + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } + }, + + /** + * Verify if directory exists in folder tree + * + * @param {String} path + */ + folderExistsInTree: function (path) { + if (!_.isUndefined(path)) { + return $('#' + path.replace(/\//g, '\\/')).length === 1; + } + + return false; + }, + + /** + * Check if need to select directory by filters state + * + * @param {String} currentFilterPath + */ + isFiltersApplied: function (currentFilterPath) { + return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' && + currentFilterPath !== 'wysiwyg' && currentFilterPath !== 'catalog/category'; + }, + + /** + * Locate and higlight node in jstree by path id. + * + * @param {String} path + */ + locateNode: function (path) { + var selectedId = $(this.directoryTreeSelector).jstree('get_selected').attr('id'); + + if (path === selectedId) { + return; + } + path = path.replace(/\//g, '\\/'); + $(this.directoryTreeSelector).jstree('open_node', '#' + path); + $(this.directoryTreeSelector).jstree('select_node', '#' + path, true); + + }, + + /** + * Listener to clear filters event + */ + clearFiltersHandle: function () { + if (_.isUndefined(this.filterChips().filters.path)) { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); + } + }, + + /** + * Set active node filter, or deselect if the same node clicked + * + * @param {String} nodePath + */ + setActiveNodeFilter: function (nodePath) { + + if (this.activeNode() === nodePath && !this.jsTreeReloaded) { + this.selectStorageRoot(); + } else { + this.selectFolder(nodePath); + } + }, + + /** + * Remove folders selection -> select storage root + */ + selectStorageRoot: function () { + var filters = {}, + applied = this.filterChips().get('applied'); + + $(this.directoryTreeSelector).jstree('deselect_all'); + + filters = $.extend(true, filters, applied); + delete filters.path; + this.filterChips().set('applied', filters); + this.activeNode(null); + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setInActive(); + }.bind(this) + ); + + }, + + /** + * Set selected folder + * + * @param {String} path + */ + selectFolder: function (path) { + this.activeNode(path); + + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setActive(path); + }.bind(this) + ); + + this.applyFilter(path); + }, + + /** + * Remove active node from directory tree, and select next + */ + removeNode: function () { + $(this.directoryTreeSelector).jstree('remove'); + }, + + /** + * Apply folder filter by path + * + * @param {String} path + */ + applyFilter: function (path) { + var filters = {}, + applied = this.filterChips().get('applied'); + + filters = $.extend(true, filters, applied); + filters.path = path; + this.filterChips().set('applied', filters); + + }, + + /** + * Reload jstree and update jstree events + */ + reloadJsTree: function () { + var deferred = $.Deferred(); + + this.getJsonTree().then(function (data) { + this.createTree(data); + this.setJsTreeReloaded(true); + this.initEvents(); + deferred.resolve(); + }.bind(this)); + + return deferred.promise(); + }, + + /** + * Get json data for jstree + */ + getJsonTree: function () { + var deferred = $.Deferred(); + + $.ajax({ + url: this.getDirectoryTreeUrl, + type: 'GET', + dataType: 'json', + + /** + * Success handler for request + * + * @param {Object} data + */ + success: function (data) { + deferred.resolve(data); + }, + + /** + * Error handler for request + * + * @param {Object} jqXHR + * @param {String} textStatus + */ + error: function (jqXHR, textStatus) { + deferred.reject(); + throw textStatus; + } + }); + + return deferred.promise(); + }, + + /** + * Initialize directory tree + * + * @param {Array} data + */ + createTree: function (data) { + $(this.directoryTreeSelector).jstree({ + plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], + vcheckbox: { + 'two_state': true, + 'real_checkboxes': true + }, + 'json_data': { + data: data + }, + hotkeys: { + space: this._changeState, + 'return': this._changeState + }, + types: { + 'types': { + 'disabled': { + 'check_node': true, + 'uncheck_node': true + } + } + } + }); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js new file mode 100644 index 0000000000000..e7c05573a4f11 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -0,0 +1,288 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/grid/columns/column', + 'uiLayout', + 'underscore' +], function ($, Column, layout, _) { + 'use strict'; + + return Column.extend({ + defaults: { + bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image', + deleteImageUrl: 'media_gallery/image/delete', + addSelectedBtnSelector: '#add_selected', + deleteSelectedBtnSelector: '#delete_selected', + selected: null, + fields: { + id: 'id', + url: 'url', + alt: 'name' + }, + modules: { + actions: '${ $.name }_actions', + provider: '${ $.provider }', + messages: '${ $.messagesName }', + massaction: '${ $.massactionComponentName }' + }, + imports: { + activeDirectory: '${ $.mediaGalleryDirectoryComponent }:activeNode' + }, + listens: { + activeDirectory: 'selectDirectoryHandle', + '${ $.massactionComponentName }:massActionMode': 'updateSelected' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', + name: '${ $.name }_actions', + imageModelName: '${ $.name }' + } + ] + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initView(); + $(window).on('fileDeleted.enhancedMediaGallery', this.reloadMediaGrid.bind(this)); + $(window).on('reload.MediaGallery', this.reloadGrid.bind(this)); + + return this; + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'selected' + ]); + + return this; + }, + + /** + * Is massaction mode active. + */ + isMassActionMode: function () { + return this.massaction().massActionMode(); + }, + + /** + * Returns url to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getUrl: function (record) { + return record[this.fields.url]; + }, + + /** + * Returns id to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Number} + */ + getId: function (record) { + return record[this.fields.id]; + }, + + /** + * Update selected items per massaction mode. + */ + updateSelected: function () { + this.selected({}); + }, + + /** + * Returns name to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getImageAlt: function (record) { + return record[this.fields.alt]; + }, + + /** + * Check if the record is currently selected + * + * @param {Object} record - Data to be preprocessed. + * @returns {Boolean} + */ + isSelected: function (record) { + if (_.isNull(this.selected())) { + return false; + } + + if (this.massaction().massActionMode()) { + return this.selected()[record.id]; + } + + return this.getId(this.selected()) === this.getId(record); + }, + + /** + * Click on image + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnImage: function (record, collapsibleOpened) { + if (!collapsibleOpened) { + this.select(record); + } + }, + + /** + * Click on three-dots + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnThreeDots: function (record, collapsibleOpened) { + if (!this.isSelected(record) || collapsibleOpened) { + this.select(record); + } + }, + + /** + * Handle checkbox click. + */ + checkboxClick: function (record) { + var items = this.selected(); + + if (this.selected()[record.id]) { + delete items[record.id]; + this.selected(items); + } else { + items[record.id] = record.id; + this.selected(items); + } + + return true; + }, + + /** + * Set the record as selected + */ + select: function (record) { + if (this.massaction().massActionMode()) { + return this.checkboxClick(record); + } + + this.isSelected(record) ? this.selected(null) : this.selected(record); + this.toggleAddSelectedButton(); + + return true; + }, + + /** + * Deselect the record + */ + deselectImage: function () { + this.selected(null); + this.toggleAddSelectedButton(); + }, + + /** + * Get the selected record + * @returns {Object} + */ + getSelected: function () { + return this.selected(); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Toggle add selected button + */ + toggleAddSelectedButton: function () { + if (this.selected() === null) { + this.hideAddSelectedAndDeleteButon(); + } else { + $(this.addSelectedBtnSelector).removeClass('no-display'); + $(this.deleteSelectedBtnSelector).removeClass('no-display'); + } + }, + + /** + * Hide add selected and Delete button + */ + hideAddSelectedAndDeleteButon: function () { + $(this.addSelectedBtnSelector).addClass('no-display'); + $(this.deleteSelectedBtnSelector).addClass('no-display'); + }, + + /** + * @param {jQuery.event} e + * @param {Object} data + */ + reloadMediaGrid: function (e, data) { + if (data.reload) { + this.reloadGrid(); + } + + if (data.message && data.code) { + this.addMessage(data.code, data.message); + } + this.hideAddSelectedAndDeleteButon(); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + }, + + /** + * Add message + * + * @param {String} code + * @param {String} message + */ + addMessage: function (code, message) { + this.messages().add(code, message); + this.messages().scheduleCleanup(); + }, + + /** + * Listener to select directory event + * + * @param {String} path + */ + selectDirectoryHandle: function (path) { + if (this.selected() && + this.selected().directory !== path && + !this.massaction().massActionMode()) { + this.deselectImage(); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js new file mode 100644 index 0000000000000..38743c8d83d3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -0,0 +1,109 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'mage/translate' +], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/columns/image/actions', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + actionsList: [ + { + name: 'image-details', + title: $t('View Details'), + handler: 'viewImageDetails' + }, + { + name: 'edit', + title: $t('Edit'), + handler: 'editImageDetails' + }, + { + name: 'delete', + title: $t('Delete'), + handler: 'deleteImageAction' + } + ], + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize image action events + */ + initEvents: function () { + $(this.imageModel().addSelectedBtnSelector).click(function () { + image.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + }.bind(this)); + $(this.imageModel().deleteSelectedBtnSelector).click(function () { + this.deleteImageAction(this.imageModel().selected()); + }.bind(this)); + + }, + + /** + * Delete image action + * + * @param {Object} record + */ + deleteImageAction: function (record) { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction([record.id], imageDetailsUrl, deleteImageUrl); + }, + + /** + * View image details + * + * @param {Object} record + */ + viewImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryImageDetails().showImageDetailsById(recordId); + }, + + /** + * Edit image details + * + * @param {Object} record + */ + editImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryEditDetails().showEditDetailsPanel(recordId); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js new file mode 100644 index 0000000000000..f72a05b6d2709 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js @@ -0,0 +1,131 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global FORM_KEY, tinyMceEditors */ +define([ + 'jquery', + 'wysiwygAdapter', + 'underscore', + 'mage/translate' +], function ($, wysiwyg, _, $t) { + 'use strict'; + + return { + + /** + * Insert provided image in wysiwyg if enabled, or widget + * + * @param {Object} record + * @param {Object} config + * @returns {Boolean} + */ + insertImage: function (record, config) { + var targetElement; + + if (record === null) { + return false; + } + targetElement = this.getTargetElement(window.MediabrowserUtility.targetElementId); + + if (!targetElement.length) { + window.MediabrowserUtility.closeDialog(); + throw $t('Target element not found for content update'); + } + + $.ajax({ + url: config.onInsertUrl, + data: { + filename: record['encoded_id'], + 'store_id': config.storeId, + 'as_is': targetElement.is('textarea') ? 1 : 0, + 'force_static_path': targetElement.data('force_static_path') ? 1 : 0, + 'form_key': FORM_KEY + }, + context: this, + showLoader: true + }).done($.proxy(function (data) { + if (targetElement.is('textarea')) { + this.insertAtCursor(targetElement.get(0), data); + targetElement.focus(); + $(targetElement).change(); + } else { + targetElement.val(data) + .data('size', record.size) + .data('mime-type', record['content_type']) + .trigger('change'); + } + }, this)); + window.MediabrowserUtility.closeDialog(); + targetElement.focus(); + }, + + /** + * Insert image to target instance. + * + * @param {Object} element + * @param {*} value + */ + insertAtCursor: function (element, value) { + var sel, startPos, endPos, scrollTop; + + if ('selection' in document) { + //For browsers like Internet Explorer + element.focus(); + sel = document.selection.createRange(); + sel.text = value; + element.focus(); + } else if (element.selectionStart || element.selectionStart == '0') { //eslint-disable-line eqeqeq + //For browsers like Firefox and Webkit based + startPos = element.selectionStart; + endPos = element.selectionEnd; + scrollTop = element.scrollTop; + element.value = element.value.substring(0, startPos) + value + + element.value.substring(startPos, endPos) + element.value.substring(endPos, element.value.length); + element.focus(); + element.selectionStart = startPos + value.length; + element.selectionEnd = startPos + value.length + element.value.substring(startPos, endPos).length; + element.scrollTop = scrollTop; + } else { + element.value += value; + element.focus(); + } + }, + + /** + * Return opener Window object if it exists, not closed and editor is active + * + * @param {String} targetElementId + * return {Object|null} + */ + getMediaBrowserOpener: function (targetElementId) { + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId) && !_.isUndefined(tinyMceEditors) && + !tinyMceEditors.get(targetElementId).getMediaBrowserOpener().closed + ) { + return tinyMceEditors.get(targetElementId).getMediaBrowserOpener(); + } + + return null; + }, + + /** + * Get target element + * + * @param {String} targetElementId + * @returns {*|n.fn.init|jQuery|HTMLElement} + */ + getTargetElement: function (targetElementId) { + var opener; + + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId)) { + opener = this.getMediaBrowserOpener(targetElementId) || window; + targetElementId = tinyMceEditors.get(targetElementId).getMediaBrowserTargetElementId(); + + return $(opener.document.getElementById(targetElementId)); + } + + return $('#' + targetElementId); + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js new file mode 100644 index 0000000000000..659fcc0cdcfda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js @@ -0,0 +1,49 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/masonry', + 'jquery' +], function (Masonry, $) { + 'use strict'; + + return Masonry.extend({ + defaults: { + modules: { + provider: '${ $.provider }' + } + }, + + /** + * Init component + * + * @return {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize events + */ + initEvents: function () { + $(window).on('folderDeleted.enhancedMediaGallery', this.reloadGrid.bind(this)); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js new file mode 100644 index 0000000000000..a49303669edc8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js @@ -0,0 +1,147 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'mage/translate', + 'text!Magento_MediaGalleryUi/template/grid/massactions/cancelButton.html' +], function ($, Component, $t, cancelMassActionButton) { + 'use strict'; + + return Component.extend({ + defaults: { + pageActionsSelector: '.page-actions-buttons', + gridSelector: '[data-id="media-gallery-masonry-grid"]', + originDeleteSelector: null, + originCancelEvent: null, + cancelMassactionButton: cancelMassActionButton, + isCancelButtonInserted: false, + deleteButtonSelector: '#delete_massaction', + addSelectedButtonSelector: '#add_selected', + cancelMassactionButtonSelector: '#cancel', + standAloneTitle: 'Manage Gallery', + slidePanelTitle: 'Media Gallery', + defaultTitle: null, + contextButtonSelector: '.three-dots', + buttonsIds: [ + '#delete_folder', + '#create_folder', + '#upload_image', + '#search_adobe_stock', + '.three-dots', + '#add_selected' + ], + massactionModeTitle: $t('Select Images to Delete') + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + + return this; + }, + + /** + * Switch massaction view state per active mode. + */ + switchView: function () { + this.changePageTitle(); + this.switchButtons(); + }, + + /** + * Hide or show buttons per active mode. + */ + switchButtons: function () { + + if (this.massActionMode()) { + this.activateMassactionButtonView(); + } else { + this.revertButtonsToDefaultView(); + } + }, + + /** + * Sets buttons to default regular -mode view. + */ + revertButtonsToDefaultView: function () { + $(this.deleteButtonSelector).replaceWith(this.originDeleteSelector); + + if (!this.isCancelButtonInserted) { + $('#cancel_massaction').replaceWith(this.originCancelEvent); + } else { + $(this.cancelMassactionButtonSelector).addClass('no-display'); + $('#cancel_massaction').remove(); + } + + $.each(this.buttonsIds, function (key, value) { + $(value).removeClass('no-display'); + }); + + $(this.addSelectedButtonSelector).addClass('no-display'); + $(this.deleteButtonSelector) + .addClass('media-gallery-actions-buttons') + .removeClass('primary'); + }, + + /** + * Activate mass action buttons view + */ + activateMassactionButtonView: function () { + this.originDeleteSelector = $(this.deleteButtonSelector).clone(); + $(this.originDeleteSelector).click(function () { + $(window).trigger('massAction.MediaGallery'); + }); + this.originCancelEvent = $('#cancel').clone(true, true); + + $.each(this.buttonsIds, function (key, value) { + $(value).addClass('no-display'); + }); + + $(this.deleteButtonSelector) + .removeClass('media-gallery-actions-buttons') + .text($t('Delete Selected')) + .addClass('primary'); + + if (!$(this.cancelMassactionButtonSelector).length) { + $(this.pageActionsSelector).append(this.cancelMassactionButton); + this.isCancelButtonInserted = true; + } else { + $(this.cancelMassactionButtonSelector).replaceWith(this.cancelMassactionButton); + } + $('#cancel_massaction').on('click', function () { + $(window).trigger('terminateMassAction.MediaGallery'); + }).applyBindings(); + + $(this.deleteButtonSelector).off('click').on('click', function () { + $(this.deleteButtonSelector).trigger('massDelete'); + }.bind(this)); + + }, + + /** + * Change page title per active mode. + */ + changePageTitle: function () { + var title = $('h1:contains(' + this.standAloneTitle + ')'), + titleSelector = title.length === 1 ? title : $('h1:contains(' + this.slidePanelTitle + ')'); + + if (this.massActionMode()) { + this.defaultTitle = titleSelector.text(); + titleSelector.text(this.massactionModeTitle); + } else { + titleSelector = $('h1:contains(' + this.massactionModeTitle + ')'); + titleSelector.text(this.defaultTitle); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js new file mode 100644 index 0000000000000..8114305a3b29c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -0,0 +1,151 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'uiLayout', + 'underscore', + 'Magento_Ui/js/modal/alert', + 'mage/translate' +], function ($, Component, DeleteImages, Layout, _, uiAlert, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + deleteImagesSelector: '#delete_massaction', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + modules: { + massactionView: '${ $.name }_view', + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/massaction/massactionView', + name: '${ $.name }_view' + } + ], + imports: { + imageItems: '${ $.mediaGalleryProvider }:data.items' + }, + listens: { + imageItems: 'checkButtonVisibility' + }, + exports: { + massActionMode: '${ $.name }_view:massActionMode' + } + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + this.initView(); + this.initEvents(); + + return this; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + Layout(this.viewConfig); + + return this; + }, + + /** + * Initilize massactions events for media gallery grid. + */ + initEvents: function () { + $(window).on('massAction.MediaGallery', function () { + if (this.massActionMode()) { + return; + } + this.imageModel().selected(null); + this.massActionMode(true); + this.switchMode(); + }.bind(this)); + + $(window).on('terminateMassAction.MediaGallery', function () { + if (!this.massActionMode()) { + return; + } + + this.massActionMode(false); + this.switchMode(); + }.bind(this)); + }, + + /** + * Return total selected items. + */ + getSelectedCount: function () { + if (this.massActionMode() && !_.isNull(this.imageModel().selected())) { + return Object.keys(this.imageModel().selected()).length; + } + + return 0; + }, + + /** + * If images records less than one, disable "delete images" button + */ + checkButtonVisibility: function () { + if (this.imageItems.length < 1) { + $(this.deleteImagesSelector).addClass('disabled'); + } else { + $(this.deleteImagesSelector).removeClass('disabled'); + } + }, + + /** + * Switch massaction per current event. + */ + switchMode: function () { + this.massactionView().switchView(); + this.handleDeleteAction(); + }, + + /** + * Change Default behavior of delete image to bulk deletion. + */ + handleDeleteAction: function () { + if (this.massActionMode()) { + $(this.massactionView().deleteButtonSelector).on('massDelete', function () { + if (this.getSelectedCount() < 1) { + uiAlert({ + content: $t('You need to select at least one image') + }); + + } else { + DeleteImages.deleteImageAction( + this.imageModel().selected(), + this.mediaGalleryImageDetails().imageDetailsUrl, + this.imageModel().deleteImageUrl + ).then(function (response) { + if (response.status === 'canceled') { + return; + } + this.imageModel().selected({}); + this.massActionMode(false); + this.switchMode(); + }.bind(this)); + } + }.bind(this)); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js new file mode 100644 index 0000000000000..7116784f41a0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/messages', + messageDelay: 5, + messages: [] + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'messages' + ]); + + return this; + }, + + /** + * Get messages + * + * @returns {Array} + */ + get: function () { + return this.messages(); + }, + + /** + * Add message + * + * @param {String} type + * @param {String} message + */ + add: function (type, message) { + this.messages.push({ + code: type, + message: message + }); + }, + + /** + * Clear messages + */ + clear: function () { + this.messages.removeAll(); + }, + + /** + * Schedule message cleanup + * + * @param {Number} delay + */ + scheduleCleanup: function (delay) { + // eslint-disable-next-line no-unused-vars + var timerId; + + delay = delay || this.messageDelay; + + timerId = setTimeout(function () { + clearTimeout(timerId); + this.clear(); + }.bind(this), Number(delay) * 1000); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js new file mode 100644 index 0000000000000..15f62d6a7efd1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/sortBy' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + columnIndexMap: {} + }, + + /** + * Prepared sort order options + */ + preparedOptions: function (columns) { + var index = 0, + sortBy; + + if (columns && columns.length > 0) { + columns.map(function (column) { + if (column.sortable === true) { + sortBy = column['sort_by'] || {}; + + if (sortBy.excluded) { + return; + } + + this.options.push({ + value: column.index, + label: column.label, + sortByField: sortBy.field, + sortDirection: sortBy.direction + }); + + this.columnIndexMap[column.index] = index++; + + this.isVisible(true); + } else { + this.isVisible(false); + } + }.bind(this)); + } + }, + + /** + * Apply changes + */ + applyChanges: function () { + var column = this.getColumn(this.selectedOption()); + + this.applied({ + field: column.sortByField || this.selectedOption(), + direction: column.sortDirection || this.sorting + }); + }, + + /** + * Get column by index + * + * @param {String} optionIndex + * @returns {Object} + */ + getColumn: function (optionIndex) { + return this.options[this.columnIndexMap[optionIndex]]; + }, + + /** + * Select default option + */ + selectDefaultOption: function () { + this.selectedOption(this.options[0].value); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js new file mode 100644 index 0000000000000..3b69ca07f5771 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js @@ -0,0 +1,245 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'jquery', + 'underscore', + 'Magento_Ui/js/lib/validation/validator', + 'mage/translate', + 'jquery/file-uploader' +], function (Component, $, _, validator, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + imageUploadInputSelector: '#image-uploader-form', + directoriesPath: 'media_gallery_listing.media_gallery_listing.media_gallery_directories', + actionsPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url', + messagesPath: 'media_gallery_listing.media_gallery_listing.messages', + imageUploadUrl: '', + acceptFileTypes: '', + allowedExtensions: '', + maxFileSize: '', + maxFileNameLength: 90, + loader: false, + modules: { + directories: '${ $.directoriesPath }', + actions: '${ $.actionsPath }', + mediaGridMessages: '${ $.messagesPath }', + sortBy: '${ $.sortByName }', + listingPaging: '${ $.listingPagingName }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super().observe( + [ + 'loader', + 'count' + ] + ); + + return this; + }, + + /** + * Initializes file upload library + */ + initializeFileUpload: function () { + $(this.imageUploadInputSelector).fileupload({ + url: this.imageUploadUrl, + acceptFileTypes: this.acceptFileTypes, + allowedExtensions: this.allowedExtensions, + maxFileSize: this.maxFileSize, + + /** + * Extending the form data + * + * @param {Object} form + * @returns {Array} + */ + formData: function (form) { + return form.serializeArray().concat( + [{ + name: 'isAjax', + value: true + }, + { + name: 'form_key', + value: window.FORM_KEY + }, + { + name: 'target_folder', + value: this.getTargetFolder() + }] + ); + }.bind(this), + + add: function (e, data) { + if (!this.isSizeExceeded(data.files[0]).passed) { + this.addValidationErrorMessage('Cannot upload "' + data.files[0].name + + '". File exceeds maximum file size limit.'); + + return; + } else if (!this.isFileNameLengthExceeded(data.files[0]).passed) { + this.addValidationErrorMessage('Cannot upload "' + data.files[0].name + + '". Filename is too long, must be 90 characters or less.'); + + return; + } + + this.showLoader(); + this.count(1); + data.submit(); + }.bind(this), + + stop: function () { + this.openNewestImages(); + this.mediaGridMessages().scheduleCleanup(); + }.bind(this), + + start: function () { + this.mediaGridMessages().clear(); + }.bind(this), + + done: function (e, data) { + var response = data.jqXHR.responseJSON; + + if (!response) { + this.showErrorMessage(data, $t('Could not upload the asset.')); + + return; + } + + if (!response.success) { + this.showErrorMessage(data, response.message); + + return; + } + this.showSuccessMessage(data); + this.hideLoader(); + this.actions().reloadGrid(); + }.bind(this) + }); + }, + + /** + * Add error message after validation error. + * + * @param {String} message + */ + addValidationErrorMessage: function (message) { + this.mediaGridMessages().add( + 'error', + $t(message) + ); + + this.count() < 2 || this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Checks if size of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isSizeExceeded: function (file) { + return validator('validate-max-size', file.size, this.maxFileSize); + }, + + /** + * Checks if name length of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isFileNameLengthExceeded: function (file) { + return validator('max_text_length', file.name, this.maxFileNameLength); + }, + + /** + * Go to recently uploaded images if at least one uploaded successfully + */ + openNewestImages: function () { + this.mediaGridMessages().get().each(function (message) { + if (message.code === 'success') { + this.actions().deselectImage(); + this.sortBy().selectDefaultOption(); + this.listingPaging().goFirst(); + + return false; + } + }.bind(this)); + }, + + /** + * Show error meassages with file name. + * + * @param {Object} data + * @param {String} message + */ + showErrorMessage: function (data, message) { + data.files.each(function (file) { + this.mediaGridMessages().add( + 'error', + file.name + ': ' + $t(message) + ); + }.bind(this)); + + this.hideLoader(); + }, + + /** + * Show success message, and files counts + */ + showSuccessMessage: function () { + var prefix = this.count() === 1 ? 'an image' : this.count() + ' images'; + + this.mediaGridMessages().messages.remove(function (item) { + return item.code === 'success'; + }); + this.mediaGridMessages().add('success', $t('Successfully uploaded ' + prefix)); + this.count(this.count() + 1); + + }, + + /** + * Gets Media Gallery selected folder + * + * @returns {String} + */ + getTargetFolder: function () { + + if (_.isUndefined(this.directories().activeNode()) || + _.isNull(this.directories().activeNode())) { + return '/'; + } + + return this.directories().activeNode(); + }, + + /** + * Shows spinner loader + */ + showLoader: function () { + this.loader(true); + }, + + /** + * Hides spinner loader + */ + hideLoader: function () { + this.loader(false); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js new file mode 100644 index 0000000000000..c7ca95bed863c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiElement', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'Magento_MediaGalleryUi/js/action/saveDetails', + 'mage/validation' +], function ($, _, Element, deleteImageWithDetailConfirmation, addSelected, saveDetails) { + 'use strict'; + + return Element.extend({ + defaults: { + modalSelector: '', + modalWindowSelector: '', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + template: 'Magento_MediaGalleryUi/image/actions', + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + $(window).on('fileDeleted.enhancedMediaGallery', this.closeViewDetailsModal.bind(this)); + + return this; + }, + + /** + * Close the images details modal + */ + closeModal: function () { + var modalElement = $(this.modalSelector), + modalWindow = $(this.modalWindowSelector); + + if (!modalWindow.hasClass('_show') || !modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Opens the image edit panel + */ + editImageAction: function () { + var record = this.imageModel().getSelected().id; + + this.mediaGalleryEditDetails().showEditDetailsPanel(record); + }, + + /** + * Delete image action + */ + deleteImageAction: function () { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction( + [this.imageModel().getSelected().id], + imageDetailsUrl, + deleteImageUrl + ); + }, + + /** + * Save image details action + */ + saveImageDetailsAction: function () { + var saveDetailsUrl = this.mediaGalleryEditDetails().saveDetailsUrl, + modalElement = $(this.modalSelector), + form = modalElement.find('#image-edit-details-form'), + imageId = this.imageModel().getSelected().id, + keywords = this.mediaGalleryEditDetails().selectedKeywords(), + imageDetails = this.mediaGalleryImageDetails(); + + if (form.validation('isValid')) { + saveDetails( + saveDetailsUrl, + [form.serialize(), $.param({ + 'keywords': keywords + })].join('&') + ).then(function () { + this.closeModal(); + this.imageModel().reloadGrid(); + imageDetails.removeCached(imageId); + + if (imageDetails.isActive()) { + imageDetails.showImageDetailsById(imageId); + } + }.bind(this)); + } + }, + + /** + * Add Image + */ + addImage: function () { + addSelected.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + this.closeModal(); + }, + + /** + * Close view details modal after confirm deleting image + */ + closeViewDetailsModal: function () { + this.closeModal(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js new file mode 100644 index 0000000000000..d0d37d49329e0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js @@ -0,0 +1,174 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/getDetails' +], function ($, _, Component, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-details', + modalSelector: '', + modalWindowSelector: '', + imageDetailsUrl: '/media_gallery/image/details', + images: [], + tagListLimit: 7, + showAllTags: false, + image: null, + modules: { + mediaGridMessages: '${ $.mediaGridMessages }' + } + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'showAllTags' + ]); + + return this; + }, + + /** + * Show image details by ID + * + * @param {String} imageId + */ + showImageDetailsById: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }, + + /** + * Open image details popup + */ + openImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.showAllTags(false); + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Get tag text + * + * @param {String} tagText + * @param {Number} tagIndex + * @return {String} + */ + getTagText: function (tagText, tagIndex) { + return tagText + (this.image().tags.length - 1 === tagIndex ? '' : ','); + }, + + /** + * Show all image tags + */ + showMoreImageTags: function () { + this.showAllTags(true); + }, + + /** + * Is value an object + * + * @param {*} value + * @returns {Boolean} + */ + isArray: function (value) { + return _.isArray(value); + }, + + /** + * Get name and number text for used in link + * + * @param {Object} item + * @returns {String} + */ + getUsedInText: function (item) { + return item.name + '(' + item.number + ')'; + }, + + /** + * Get filter url + * + * @param {String} link + */ + getFilterUrl: function (link) { + return link + '?filters[asset_id]=' + this.image().id; + }, + + /** + * Check if details modal is active + * @return {Boolean} + */ + isActive: function () { + return $(this.modalWindowSelector).hasClass('_show'); + }, + + /** + * Remove image details + * + * @param {String} id + */ + removeCached: function (id) { + delete this.images[id]; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js new file mode 100644 index 0000000000000..c31bc848bdc70 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js @@ -0,0 +1,228 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'uiLayout', + 'Magento_Ui/js/lib/key-codes', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'mage/validation' +], function ($, _, Component, layout, keyCodes, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-edit', + modalSelector: '.media-gallery-edit-image-details-modal', + imageEditDetailsUrl: '/media_gallery/image/details', + saveDetailsUrl: '/media_gallery/image/saveDetails', + images: [], + image: null, + keywordOptions: [], + selectedKeywords: [], + newKeyword: '', + newKeywordSelector: '#keyword', + modules: { + mediaGridMessages: '${ $.mediaGridMessages }', + keywordsSelect: '${ $.name }_keywords' + }, + viewConfig: [ + { + component: 'Magento_Ui/js/form/element/ui-select', + name: '${ $.name }_keywords', + template: 'ui/grid/filters/elements/ui-select', + disableLabel: true + } + ], + exports: { + keywordOptions: '${ $.name }_keywords:options' + }, + links: { + selectedKeywords: '${ $.name }_keywords:value' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super().initView(); + + return this; + }, + + /** + * Add a new keyword to select + */ + addKeyword: function () { + var options = this.keywordOptions(), + selected = this.selectedKeywords(), + newKeywordField = $(this.newKeywordSelector); + + newKeywordField.validation(); + + if (!newKeywordField.validation('isValid') || this.newKeyword() === '') { + return; + } + + options.push(this.getOptionForKeyword(this.newKeyword())); + selected.push(this.newKeyword()); + this.newKeyword(''); + + this.keywordOptions(options); + this.selectedKeywords(selected); + }, + + /** + * Create an option object based on keyword string + * + * @param {String} keyword + * @returns {Object} + */ + getOptionForKeyword: function (keyword) { + return { + 'is_active': 1, + level: 1, + value: keyword, + label: keyword + }; + }, + + /** + * Convert array of keywords to options format + * + * @param {Array} tags + */ + setKeywordOptions: function (tags) { + var options = []; + + tags.forEach(function (tag) { + options.push(this.getOptionForKeyword(tag)); + }.bind(this)); + + this.keywordOptions(options); + this.selectedKeywords(tags); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'keywordOptions', + 'selectedKeywords', + 'newKeyword' + ]); + + return this; + }, + + /** + * Get image details by ID + * + * @param {String} imageId + */ + showEditDetailsPanel: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageEditDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openEditImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }, + + /** + * Open edit image details popup + */ + openEditImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.setKeywordOptions(this.image().tags); + this.newKeyword(''); + + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Handle Enter key event to save image details + * + * @param {Object} data + * @param {jQuery.Event} event + * @returns {Boolean} + */ + handleEnterKey: function (data, event) { + var modalElement = $(this.modalSelector), + key = keyCodes[event.keyCode]; + + if (key === 'enterKey') { + event.preventDefault(); + modalElement.find('.page-action-buttons button.save').click(); + } + + return true; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js new file mode 100644 index 0000000000000..127f1676015f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-description', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\n\ ]+$|^$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), ' + + 'dots (.), commas(,), underscores (_), dashes (-), and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js new file mode 100644 index 0000000000000..47fa5b19781bc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($, validate, $t) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-keyword', function (value) { + return /^[a-zA-Z0-9\-\_\.\,]+$|^$/i.test(value); + + }, $t('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_) and dashes(-) on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js new file mode 100644 index 0000000000000..1429be64b7d12 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-title', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\ ]+$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_), dashes(-) and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html new file mode 100644 index 0000000000000..4a8350231a0fd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html @@ -0,0 +1,45 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-wrap" collapsible> + <div class="mediagallery-massaction-checkbox" if="isMassActionMode()"> + <input type="checkbox" attr="{ 'data-ui-id': $row().title }" visible="isMassActionMode()" ko-checked="isSelected($row())" click="function () { return select($row()); }"/> + </div> + <div class="media-gallery-image"> + <div data-row="file" + class="masonry-image-block media-gallery-image-block" + attr="'data-id': $col.getId($row())" + css="{ selected: isSelected($row()) }" + click="function(){ clickOnImage($row(), $collapsible.opened()) }" + > + <img attr="src: $col.getUrl($row()), alt: $col.getImageAlt($row())" + class="media-gallery-image-column" + data-role="thumbnail"/> + </div> + <ul class="action-menu" css="_active: $collapsible.opened"> + <scope args="actions"> + <render args="template"/> + </scope> + </ul> + </div> + <div class="masonry-image-description"> + <ul class="media-gallery-image-details"> + <li class="name" data-ui-id="title" text="$row().title"></li> + <li class="source"> + <img if="$row().source" class="adobe-stock-icon" attr="{ src: $row().source }"/> + </li> + <li class="type" data-ui-id="content-type" text="$row().content_type"></li> • + <li class="dimensions" data-ui-id="dimensions" text="$row().width + 'x' + $row().height"></li> + </ul> + <div class="media-gallery-image-actions"> + <div class="action-select-wrap"> + <span class="three-dots" ifnot="isMassActionMode()" + toggleCollapsible + click="function () { clickOnThreeDots($row(), $collapsible.opened()); }"></span> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html new file mode 100644 index 0000000000000..042e119b9f40e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<each args="{ data: actionsList, as: 'action' }"> + <li> + <a class="action-menu-item" href="" text="action.title" + click="$parent[action.handler].bind($parent, $row())" + attr="{'data-action': 'item-' + action.name}"> + </a> + </li> +</each> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html new file mode 100644 index 0000000000000..da835952e2f23 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html @@ -0,0 +1,10 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="media-directory-container"> + <div id="media-gallery-directory-tree"></div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html new file mode 100644 index 0000000000000..d1840fdb3dc8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html @@ -0,0 +1,24 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__field admin__media-gallery-image-checkbox" visible="visible" css="$data.additionalClasses"> + <div class="admin__field-control"> + <label class="admin__form-field-label" if="$data.label" attr="for: uid"> + <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> + </label> + </div> + <div class="admin__field admin__field-option"> + <input type="checkbox" + class="admin__control-checkbox" + ko-checked="$data.checked" + disable="disabled" + ko-value="value" + hasFocus="focused" + attr="id: uid, name: inputName"/> + + <label class="admin__field-label" text="description" attr="for: uid"/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html new file mode 100644 index 0000000000000..cce859f331d9a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html @@ -0,0 +1,133 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<ifnot args="disableLabel"> + <label class="admin__form-field-label" attr="{for: uid}"> + <span translate="label"></span> + </label> +</ifnot> +<div class="admin__action-multiselect-wrap action-select-wrap media-gallery-asset-ui-select-filter" + tabindex="0" attr="{id: uid}" css="{_active: listVisible,'admin__action-multiselect-tree': isTree()}" + event="{focusin: onFocusIn,focusout: onFocusOut,keydown: keydownSwitcher}" outerClick="outerClick.bind($data)"> + <ifnot args="chipsEnabled"> + <div class="action-select admin__action-multiselect" + data-role="advanced-select" + css="{_active: listVisible}" + click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" data-role="selected-option" + ifnot="validationLoading" css="{warning: warn().length}" text="setCaption()"> + </div> + <button if="isRemoveSelectedIcon && hasData() || !validationLoading" class="action-close" + type="button" data-action="remove-selected-item" tabindex="-1" click="clear"> + <span class="action-close-text" translate="'Close'"></span> + </button> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="validationLoading" + if="validationLoading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + </div> + </ifnot> + <if args="chipsEnabled"> + <div class="action-select admin__action-multiselect" data-role="advanced-select" + css="{_active: listVisible}" click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" visible="!hasData()" + translate="selectedPlaceholders.defaultPlaceholder"> + </div> + <each args="{ data: getSelected(), as: 'option'}"> + <span class="admin__action-multiselect-crumb"> + <span text="label"> + </span> + <button class="action-close" type="button" data-action="remove-selected-item" + tabindex="-1" click="$parent.removeSelected.bind($parent, value)"> + <span class="action-close-text" translate="'Close'"></span> + </button> + </span> + </each> + </div> + </if> + <div class="action-menu" css="{ _active: listVisible}"> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loading" if="loading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + <if args="filterOptions"> + <div class="admin__action-multiselect-search-wrap"> + <input class="admin__control-text admin__action-multiselect-search" data-role="advanced-select-text" + type="text" event="{keydown: filterOptionsKeydown}" attr="{id: uid+2, placeholder: filterPlaceholder}" + ko-focused="filterOptionsFocus" ko-value="filterInputValue" data-bind="valueUpdate:'afterkeydown'"> + <label class="admin__action-multiselect-search-label" + data-action="advanced-select-search" + attr="{for: uid+2}"> + </label> + <div if="itemsQuantity" + text="itemsQuantity" + class="admin__action-multiselect-search-count"> + </div> + </div> + <div ifnot="options().length" + class="admin__action-multiselect-empty-area"> + <ul text="emptyOptionsHtml"/> + </div> + </if> + <ul class="admin__action-multiselect-menu-inner _root" + event="{mousemove: function(data, event){onMousemove($data, $index(), event)}, + scroll: function(data, event){onScrollDown(data, event)}}"> + <each args="{ data: options, as: 'option'}"> + <li class="admin__action-multiselect-menu-inner-item _root" + css="{ _parent: $data.optgroup }" + data-role="option-group"> + <div class="action-menu-item" + css="{ + _selected: $parent.isSelectedValue(option), + _hover: $parent.isHovered(option, $element), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), + _unclickable: $parent.isLabelDecoration($data), + _last: $parent.addLastElement($data), + '_with-checkbox': $parent.showCheckbox + }" + click="function(data, event){ + $parent.toggleOptionSelected($data, $index(), event); + }" + data-bind="clickBubble:false"> + <if args="$data.optgroup && $parent.showOpenLevelsActionIcon"> + <div class="admin__action-multiselect-dropdown" + click="function(event){ $parent.showLevels($data); $parent.openChildLevel($data, $element, event);}" + data-bind="clickBubble:false"> + </div> + </if> + <if args="$parent.showCheckbox"> + <input class="admin__control-checkbox" type="checkbox" + tabindex="-1" attr="{ 'checked': $parent.isSelected(option.value) }"> + </if> + <label class="admin__action-multiselect-label"> + <span text="option.label"></span> + <img if="$parent.getPath(option)" + class="admin__action-multiselect-item-path" + attr="{ src: option.path }"/> + </label> + </div> + <if args="$data.optgroup"> + <render args="{name: $parent.optgroupTmpl, data: {root: $parent, current: $data}}" ></render> + </if> + </li> + </each> + </ul> + <if args="$data.closeBtn"> + <div class="admin__action-multiselect-actions-wrap"> + <button class="action-default" + data-action="close-advanced-select" + type="button" + click="outerClick"> + <span translate="closeBtnLabel"></span> + </button> + </div> + </if> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html new file mode 100644 index 0000000000000..243ed1c2a5dc4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html @@ -0,0 +1,10 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<button id="cancel_massaction" type="button" class="cancel"> + <span data-bind="i18n: 'Cancel'"/> +</button> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html new file mode 100644 index 0000000000000..5bbdafebe4095 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div visible="massActionMode()" class="mediagallery-massaction-items-count"> + <div class="selected_count_text">(<b><text args="getSelectedCount()"/> Selected</b>) </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html new file mode 100644 index 0000000000000..1ec084e223e98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<ul class="messages"> + <div class="messages" outereach="messages"> + <div attr="class: 'message message-'+code"> + <div data-ui-id="messages-message-error"> + <span text="message"></span> + </div> + </div> + </div> +</ul> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html new file mode 100644 index 0000000000000..fb7334a7b0d06 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html @@ -0,0 +1,32 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__data-grid-header" data-role="masonry-main-toolbar" afterRender="$data.setToolbarNode"> + <div class="admin__data-grid-header-row"> + <div class="admin__data-grid-actions-wrap" each="getRegion('dataGridActions')" render=""/> + <each args="getRegion('dataGridFilters')" render=""/> + </div> + <div class="admin__data-grid-header-row row row-gutter"> + <div class="col-xs-2" if="hasChild('listing_massaction')" ko-scope="requestChild('listing_massaction')" render=""/> + <div css=" + 'col-xs-10': hasChild('listing_massaction'), + 'col-xs-12': !hasChild('listing_massaction')"> + <div class="row"> + <div class="col-xs-4"> + <div class="masonry-results-number" ko-scope="requestChild('listing_paging')"> + <render args="totalTmpl"/> + </div> + <each args="getRegion('sorting')" render=""/> + </div> + <div class="col-xs-8" ko-scope="requestChild('listing_paging')"> + <div render=""/> + </div> + </div> + </div> + </div> +</div> + +<render args="stickyTmpl" if="$data.sticky"/> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html new file mode 100644 index 0000000000000..6d5580b1aad6e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html @@ -0,0 +1,17 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-image-uploader-container"> + <form id="image-uploader-form" class="no-display" method="POST" enctype="multipart/form-data"> + <input afterRender="initializeFileUpload" id="image-uploader-input" type="file" name="image" + multiple="multiple"/> + </form> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loader"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html new file mode 100644 index 0000000000000..8ecaf0bd2a019 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html @@ -0,0 +1,12 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<each args="{ data: actionsList, as: 'action' }"> + <button type="button" click="$parent[action.handler].bind($parent)" + attr="{class: action.classes, id: 'image-details-action-' + action.name, title: $t(action.title)}"> + <span translate="action.title"></span> + </button> +</each> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html new file mode 100644 index 0000000000000..a397bad0af698 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html @@ -0,0 +1,65 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="image-details" if="image"> + <div class="image-details-image"> + <img attr="src: image().image_url"/> + </div> + <div class="image-details-sidebar"> + <div class="image-details-section"> + <h3 class="image-title" text="image().title"></h3> + <div class="image-type"> + <span class="source"><img if="image().source" class="adobe-stock-icon" attr="{ src: image().source }" /></span> + <span class="type" data-ui-id="content-type" text="image().content_type"></span> + </div> + </div> + <div class="filename image-details-section"> + <h3 translate="'Filename'"></h3> + <p text="image().path"></p> + </div> + <div class="general-details image-details-section" if="image().details"> + <h3 translate="'Details'"></h3> + <div class="attributes"> + <each args="image().details"> + <div class="attribute" if="value"> + <span class="title" translate="title"></span> + <ifnot args="$parent.isArray(value)"> + <div class="value" text="value"></div> + </ifnot> + <if args="$parent.isArray(value)"> + <each args="{ data: value, as: 'item'}"> + <div class="value"> + <a attr="href: $parents[1].getFilterUrl(item.link)" + text="$parents[1].getUsedInText(item)"></a> + </br> + </div> + </each> + </if> + </div> + </each> + </div> + </div> + <div class="description image-details-section" if="image().description"> + <h3 translate="'Description'"></h3> + <p text="image().description"></p> + </div> + <div class="tags image-details-section" if="image().tags.length"> + <h3 translate="'Tags'"></h3> + <div class="tags-list" css="{'show-all-tags': showAllTags}"> + <each args="data: image().tags, as: '$tag'"> + <span class="tag-item" text="$parent.getTagText($tag, $index())" + css="{'show-more-item': ($index() + 1) > $parent.tagListLimit}"></span> + </each> + </div> + <div class="show-more-link-container"> + <a href="#" class="show-more-link" if="image().tags.length > tagListLimit" + translate="'Show More'" click="showMoreImageTags"></a> + </div> + </div> + + <each args="getRegion('additional_image_details')" render=""/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html new file mode 100644 index 0000000000000..e8448e1a64aef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html @@ -0,0 +1,74 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="edit-image-details" if="image"> + <fieldset class="admin__fieldset"> + <input type="hidden" ko-value="image().id" data-ui-id="id" name="id"/> + <div class="admin__field _required"> + <label for="title" class="admin__field-label"> + <span translate="'Title'"></span> + </label> + <div class="admin__field-control"> + <input type="text" id="title" data-ui-id="title" name="title" placeholder="Title" + class="admin__control-text required-entry minimum-length-1 maximum-length-128" + ko-value="image().title" event="{keypress: handleEnterKey}" + data-validate="{'required':true,'validate-image-title':true, 'validate-length':true}"/> + </div> + </div> + <div class="admin__field"> + <label for="path" class="admin__field-label"> + <span translate="'Filename'"></span> + </label> + <div class="admin__field-control path-display"> + <span data-ui-id="path" id="path" text="image().path"></span> + </div> + </div> + <div class="admin__field"> + <label for="description" class="admin__field-label"> + <span translate="'Description'"></span> + </label> + <div class="admin__field-control"> + <textarea id="description" + data-ui-id="description" + name="description" + class="admin__control-textarea minimum-length-0 maximum-length-500" + rows="7" cols="80" + ko-value="image().description" + data-validate="{'validate-image-description':true, 'validate-length':true}"></textarea> + </div> + </div> + <div class="admin__field"> + <label class="admin__field-label"> + <span translate="'Tags'"></span> + </label> + <div class="admin__field-control"> + <div class="admin__field"> + <scope args="keywordsSelect"> + <render args="template"/> + </scope> + </div> + <div class="admin__field"> + <div class="admin__field-control admin__field-option admin__control-grouped"> + <div class="admin__field admin__field-group-additional"> + <div class="admin__field-control"> + <input type="text" id="keyword" data-ui-id="keyword" name="keyword" placeholder="New Keyword" + class="admin__control-text minimum-length-0 maximum-length-128" ko-value="newKeyword" + data-validate="{'validate-image-keyword': true, 'validate-length': true}"/> + </div> + </div> + <div class="admin__field admin__field-group-additional admin__field-small"> + <div class="admin__field-control"> + <button type="button" data-ui-id="add-keyword" class="action-basic" click="addKeyword"> + <span translate="'Add New Tag'"></span> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + </fieldset> +</div> diff --git a/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php new file mode 100644 index 0000000000000..a516ac927fd2d --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUiApi\Api; + +/** + * Class responsible to provide API access to system configuration related to the Media Gallery + */ +interface ConfigInterface +{ + /** + * Check if grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool; +} diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md new file mode 100644 index 0000000000000..005a445c68b2a --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUiApi module + +The Magento_MediaGalleryUiApi module is responsible for the media gallery user interface (UI) implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUiApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUiApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json new file mode 100644 index 0000000000000..f8d5ef11058c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-ui-api", + "description": "Magento module responsible for the media gallery UI implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUiApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUiApi/etc/module.xml b/app/code/Magento/MediaGalleryUiApi/etc/module.xml new file mode 100644 index 0000000000000..cf62515ff92b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryUiApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryUiApi/registration.php b/app/code/Magento/MediaGalleryUiApi/registration.php new file mode 100644 index 0000000000000..b3ee130a1c510 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUiApi', __DIR__); diff --git a/composer.json b/composer.json index c15dc4140f6eb..5b39c1b3f75ea 100644 --- a/composer.json +++ b/composer.json @@ -205,6 +205,20 @@ "magento/module-media-content-cms": "*", "magento/module-media-gallery": "*", "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-ui": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-integration": "*", + "magento/module-media-gallery-synchronization": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-synchronization": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-content-synchronization-catalog": "*", + "magento/module-media-content-synchronization-cms": "*", + "magento/module-media-gallery-metadata": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-catalog-ui": "*", + "magento/module-media-gallery-cms-ui": "*", + "magento/module-media-gallery-catalog-integration": "*", "magento/module-media-gallery-catalog": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", diff --git a/composer.lock b/composer.lock index 97ab2b12512c2..90131292fe956 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a8f3bda109a177996d409f39acfbfd9f", + "content-hash": "5b074864c62821207d8994a4aca444fe", "packages": [ { "name": "colinmollenhour/cache-backend-file",