From ec098d26e7735528beb8ef338b1eeb25eab3a1a1 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko Date: Mon, 3 Aug 2020 11:01:09 +0100 Subject: [PATCH] Media modules --- .../Console/Command/Synchronize.php | 70 +++ .../MediaContentSynchronization/LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/Consume.php | 37 ++ .../Model/Publish.php | 45 ++ .../Model/RemoveObsoleteContentAsset.php | 61 +++ .../ResourceModel/GetOutdatedRelations.php | 120 +++++ .../Model/Synchronize.php | 109 ++++ .../Plugin/SynchronizeMediaContent.php | 41 ++ .../MediaContentSynchronization/README.md | 14 + .../MediaContentSynchronization/composer.json | 27 + .../etc/communication.xml | 14 + .../MediaContentSynchronization/etc/di.xml | 21 + .../etc/module.xml | 10 + .../etc/queue_consumer.xml | 11 + .../etc/queue_publisher.xml | 12 + .../etc/queue_topology.xml | 14 + .../registration.php | 14 + .../Api/SynchronizeInterface.php | 21 + .../Api/SynchronizerInterface.php | 21 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/GetEntities.php | 38 ++ .../Model/GetEntitiesInterface.php | 21 + .../Model/SynchronizerPool.php | 51 ++ .../MediaContentSynchronizationApi/README.md | 13 + .../composer.json | 21 + .../MediaContentSynchronizationApi/etc/di.xml | 10 + .../etc/module.xml | 10 + .../registration.php | 14 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/Synchronizer/Category.php | 112 ++++ .../Model/Synchronizer/Product.php | 109 ++++ .../README.md | 13 + .../Model/Synchronizer/CategoryTest.php | 85 ++++ .../Model/Synchronizer/ProductTest.php | 85 ++++ .../composer.json | 24 + .../etc/di.xml | 41 ++ .../etc/module.xml | 10 + .../registration.php | 14 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/Synchronizer/Block.php | 107 ++++ .../Model/Synchronizer/Page.php | 107 ++++ .../MediaContentSynchronizationCms/README.md | 13 + .../Model/Synchronizer/BlockTest.php | 109 ++++ .../Model/Synchronizer/PageTest.php | 109 ++++ .../composer.json | 24 + .../MediaContentSynchronizationCms/etc/di.xml | 39 ++ .../etc/module.xml | 10 + .../registration.php | 14 + .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../SaveBaseCategoryImageInformation.php | 109 ++++ .../MediaGalleryCatalogIntegration/README.md | 3 + .../composer.json | 28 + .../etc/adminhtml/di.xml | 12 + .../etc/module.xml | 10 + .../registration.php | 14 + .../Controller/Adminhtml/Category/Index.php | 36 ++ .../Magento/MediaGalleryCatalogUi/LICENSE.txt | 48 ++ .../MediaGalleryCatalogUi/LICENSE_AFL.txt | 48 ++ .../Model/Listing/DataProvider.php | 199 ++++++++ .../Magento/MediaGalleryCatalogUi/README.md | 13 + ...sertCategoryGridPageDetailsActionGroup.xml | 20 + .../AdminOpenCategoryGridPageActionGroup.xml | 18 + ...nMediaGalleryCatalogUiCategoryGridPage.xml | 12 + ...diaGalleryCatalogUiCategoryGridSection.xml | 17 + ...lleryCatalogUiUsedInCategoryFilterTest.xml | 63 +++ ...alleryCatalogUiUsedInProductFilterTest.xml | 73 +++ ...eryCatalogUiVerifyCategoryGridPageTest.xml | 31 ++ .../Listing/Columns/CategoryActions.php | 66 +++ .../Ui/Component/Listing/Columns/Path.php | 80 +++ .../Component/Listing/Columns/Thumbnail.php | 91 ++++ .../MediaGalleryCatalogUi/composer.json | 26 + .../etc/adminhtml/di.xml | 41 ++ .../etc/adminhtml/routes.xml | 15 + .../MediaGalleryCatalogUi/etc/module.xml | 10 + .../MediaGalleryCatalogUi/registration.php | 14 + .../media_gallery_catalog_category_index.xml | 15 + .../media_gallery_category_listing.xml | 180 +++++++ .../ui_component/media_gallery_listing.xml | 65 +++ .../standalone_media_gallery_listing.xml | 65 +++ .../adminhtml/web/css/source/_module.less | 23 + .../Controller/Adminhtml/Block/Search.php | 97 ++++ .../Controller/Adminhtml/Page/Search.php | 97 ++++ .../Magento/MediaGalleryCmsUi/LICENSE.txt | 48 ++ .../Magento/MediaGalleryCmsUi/LICENSE_AFL.txt | 48 ++ app/code/Magento/MediaGalleryCmsUi/README.md | 13 + ...FillOutCustomCMSPageContentActionGroup.xml | 30 ++ ...ediaGalleryCmsUiUsedInBlocksFilterTest.xml | 60 +++ ...MediaGalleryCmsUiUsedInPagesFilterTest.xml | 68 +++ .../Magento/MediaGalleryCmsUi/composer.json | 23 + .../MediaGalleryCmsUi/etc/adminhtml/di.xml | 41 ++ .../etc/adminhtml/routes.xml | 15 + .../Magento/MediaGalleryCmsUi/etc/module.xml | 10 + .../MediaGalleryCmsUi/registration.php | 14 + .../ui_component/media_gallery_listing.xml | 62 +++ .../standalone_media_gallery_listing.xml | 62 +++ .../MediaGalleryIntegration/LICENSE.txt | 48 ++ .../MediaGalleryIntegration/LICENSE_AFL.txt | 48 ++ .../Model/OpenDialogUrlProvider.php | 40 ++ .../Plugin/SaveImageInformation.php | 112 ++++ .../Magento/MediaGalleryIntegration/README.md | 3 + .../Model/ImageComponentOpenDialogUrlTest.php | 72 +++ .../Model/OpenDialogUrlProviderTest.php | 63 +++ .../Model/TinyMceOpenDialogUrlTest.php | 78 +++ .../WysiwygDefaultConfigOpenDialogUrlTest.php | 82 +++ .../MediaGalleryIntegration/composer.json | 31 ++ .../etc/adminhtml/di.xml | 17 + .../MediaGalleryIntegration/etc/module.xml | 14 + .../MediaGalleryIntegration/registration.php | 10 + .../Magento/MediaGalleryMetadata/LICENSE.txt | 48 ++ .../MediaGalleryMetadata/LICENSE_AFL.txt | 48 ++ .../Model/AddIptcMetadata.php | 180 +++++++ .../Model/AddXmpMetadata.php | 104 ++++ .../MediaGalleryMetadata/Model/File.php | 79 +++ .../Model/File/AddMetadata.php | 106 ++++ .../Model/File/ExtractMetadata.php | 129 +++++ .../Model/GetIptcMetadata.php | 71 +++ .../Model/GetXmpMetadata.php | 66 +++ .../Model/Gif/ReadFile.php | 318 ++++++++++++ .../Model/Gif/Segment/ReadXmp.php | 97 ++++ .../Model/Gif/Segment/WriteXmp.php | 191 +++++++ .../Model/Gif/WriteFile.php | 76 +++ .../Model/Jpeg/ReadFile.php | 209 ++++++++ .../Model/Jpeg/Segment/ReadIptc.php | 76 +++ .../Model/Jpeg/Segment/ReadXmp.php | 85 ++++ .../Model/Jpeg/Segment/WriteIptc.php | 100 ++++ .../Model/Jpeg/Segment/WriteXmp.php | 156 ++++++ .../Model/Jpeg/WriteFile.php | 92 ++++ .../MediaGalleryMetadata/Model/Metadata.php | 95 ++++ .../Model/Png/ReadFile.php | 127 +++++ .../Model/Png/Segment/ReadIptc.php | 136 +++++ .../Model/Png/Segment/ReadXmp.php | 86 ++++ .../Model/Png/Segment/WriteIptc.php | 214 ++++++++ .../Model/Png/Segment/WriteXmp.php | 163 ++++++ .../Model/Png/WriteFile.php | 78 +++ .../MediaGalleryMetadata/Model/Segment.php | 79 +++ .../Model/SegmentNames.php | 104 ++++ .../Model/XmpTemplate.php | 58 +++ .../Magento/MediaGalleryMetadata/README.md | 3 + .../Integration/Model/AddMetadataTest.php | 197 ++++++++ .../Integration/Model/ExtractMetadataTest.php | 112 ++++ .../Integration/Model/Gif/Segment/XmpTest.php | 117 +++++ .../Model/Jpeg/Segment/IptcTest.php | 134 +++++ .../Model/Jpeg/Segment/XmpTest.php | 117 +++++ .../Model/Png/Segment/IptcTest.php | 134 +++++ .../Test/_files/empty_exiftool.gif | Bin 0 -> 7951 bytes .../Test/_files/empty_iptc.jpeg | Bin 0 -> 19416 bytes .../Test/_files/empty_iptc.png | Bin 0 -> 47596 bytes .../Test/_files/empty_xmp_image.jpeg | Bin 0 -> 19336 bytes .../Test/_files/empty_xmp_image.png | Bin 0 -> 55268 bytes .../Test/_files/exiftool.gif | Bin 0 -> 12704 bytes .../Test/_files/iptc_only.jpeg | Bin 0 -> 19884 bytes .../Test/_files/iptc_only.png | Bin 0 -> 47894 bytes .../Test/_files/macos-photos.jpeg | Bin 0 -> 22795 bytes .../Test/_files/macos-preview.png | Bin 0 -> 57535 bytes .../MediaGalleryMetadata/composer.json | 22 + .../MediaGalleryMetadata/etc/default.xmp | 24 + .../Magento/MediaGalleryMetadata/etc/di.xml | 127 +++++ .../MediaGalleryMetadata/etc/module.xml | 10 + .../MediaGalleryMetadata/registration.php | 14 + .../Api/AddMetadataInterface.php | 27 + .../Api/Data/MetadataInterface.php | 54 ++ .../Api/ExtractMetadataInterface.php | 25 + .../MediaGalleryMetadataApi/LICENSE.txt | 48 ++ .../MediaGalleryMetadataApi/LICENSE_AFL.txt | 48 ++ .../Model/AddMetadataComposite.php | 51 ++ .../Model/ExtractMetadataComposite.php | 78 +++ .../Model/FileInterface.php | 46 ++ .../Model/ReadFileInterface.php | 22 + .../Model/ReadMetadataInterface.php | 28 + .../Model/SegmentInterface.php | 46 ++ .../Model/WriteFileInterface.php | 26 + .../Model/WriteMetadataInterface.php | 24 + .../Magento/MediaGalleryMetadataApi/README.md | 3 + .../MediaGalleryMetadataApi/composer.json | 21 + .../MediaGalleryMetadataApi/etc/module.xml | 10 + .../MediaGalleryMetadataApi/registration.php | 14 + .../MediaGalleryRenditions/LICENSE.txt | 48 ++ .../MediaGalleryRenditions/LICENSE_AFL.txt | 48 ++ .../Magento/MediaGalleryRenditions/README.md | 13 + .../MediaGalleryRenditions/composer.json | 21 + .../etc/adminhtml/system.xml | 24 + .../MediaGalleryRenditions/etc/config.xml | 17 + .../MediaGalleryRenditions/etc/module.xml | 10 + .../MediaGalleryRenditions/registration.php | 14 + .../MediaGalleryRenditionsApi/LICENSE.txt | 48 ++ .../MediaGalleryRenditionsApi/LICENSE_AFL.txt | 48 ++ .../Model/Config.php | 62 +++ .../MediaGalleryRenditionsApi/README.md | 13 + .../MediaGalleryRenditionsApi/composer.json | 21 + .../MediaGalleryRenditionsApi/etc/module.xml | 10 + .../registration.php | 14 + .../Controller/Adminhtml/Asset/Search.php | 169 +++++++ .../Adminhtml/Directories/Create.php | 108 ++++ .../Adminhtml/Directories/Delete.php | 118 +++++ .../Adminhtml/Directories/GetTree.php | 80 +++ .../Controller/Adminhtml/Image/Delete.php | 143 ++++++ .../Controller/Adminhtml/Image/Details.php | 110 ++++ .../Adminhtml/Image/SaveDetails.php | 128 +++++ .../Controller/Adminhtml/Image/Upload.php | 107 ++++ .../Controller/Adminhtml/Index/Index.php | 51 ++ .../Controller/Adminhtml/Media/Index.php | 38 ++ app/code/Magento/MediaGalleryUi/LICENSE.txt | 48 ++ .../Magento/MediaGalleryUi/LICENSE_AFL.txt | 48 ++ .../Model/AssetDetailsProvider/CreatedAt.php | 59 +++ .../Model/AssetDetailsProvider/Height.php | 33 ++ .../Model/AssetDetailsProvider/Size.php | 49 ++ .../Model/AssetDetailsProvider/Type.php | 59 +++ .../Model/AssetDetailsProvider/UpdatedAt.php | 59 +++ .../Model/AssetDetailsProvider/UsedIn.php | 113 +++++ .../Model/AssetDetailsProvider/Width.php | 33 ++ .../Model/AssetDetailsProviderInterface.php | 24 + .../Model/AssetDetailsProviderPool.php | 46 ++ .../Magento/MediaGalleryUi/Model/Config.php | 48 ++ .../MediaGalleryUi/Model/DeleteImage.php | 81 +++ .../Model/Directories/FolderTree.php | 149 ++++++ .../Model/GetDetailsByAssetId.php | 144 ++++++ .../Model/Listing/DataProvider.php | 104 ++++ .../FilterProcessor/ContentField.php | 48 ++ .../FilterProcessor/Directory.php | 26 + .../FilterProcessor/Duplicated.php | 58 +++ .../FilterProcessor/Entity.php | 78 +++ .../FilterProcessor/EntityType.php | 104 ++++ .../FilterProcessor/Keyword.php | 80 +++ .../MediaGalleryUi/Model/UpdateAsset.php | 118 +++++ .../Model/UpdateAsset/SaveMetadataToFile.php | 65 +++ .../Model/UpdateAsset/UpdateKeywords.php | 81 +++ .../MediaGalleryUi/Model/UploadImage.php | 58 +++ .../Plugin/CreateThumbnails.php | 57 +++ app/code/Magento/MediaGalleryUi/README.md | 13 + ...ageInStandaloneMediaGalleryActionGroup.xml | 21 + ...eryAddImageFromImageDetailsActionGroup.xml | 19 + ...alleryApplyDuplicatedFilterActionGroup.xml | 18 + ...cedMediaGalleryApplyFiltersActionGroup.xml | 19 + ...aGalleryAssertActiveFiltersActionGroup.xml | 21 + ...ryAssertImagesDeletedInBulkActionGroup.xml | 18 + ...AssertMassActionModeDetailsActionGroup.xml | 21 + ...sertMassActionModeNotActiveActionGroup.xml | 19 + ...ssertNoActiveFiltersAppliedActionGroup.xml | 17 + ...GalleryAssertWarningMessageActionGroup.xml | 21 + ...eryCategoryGridApplyFiltersActionGroup.xml | 19 + ...eryCategoryGridExpandFilterActionGroup.xml | 19 + ...leryClickDeleteImagesButtonActionGroup.xml | 19 + ...ediaGalleryCloseViewDetailsActionGroup.xml | 19 + ...aGalleryConfirmDeleteImagesActionGroup.xml | 19 + ...dMediaGalleryDeleteGridViewActionGroup.xml | 25 + ...alleryDisableMassactionModeActionGroup.xml | 20 + ...ediaGalleryEditImageDetailsActionGroup.xml | 20 + ...GalleryEnableMassActionModeActionGroup.xml | 19 + ...cedMediaGalleryExpandFilterActionGroup.xml | 19 + ...ncedMediaGalleryImageDeleteActionGroup.xml | 22 + ...iaGalleryImageDetailsDeleteActionGroup.xml | 21 + ...ediaGalleryImageDetailsEditActionGroup.xml | 18 + ...ediaGalleryImageDetailsSaveActionGroup.xml | 24 + ...dMediaGallerySaveCustomViewActionGroup.xml | 24 + ...rySelectCustomBookmarksViewActionGroup.xml | 23 + ...erySelectImageForMassActionActionGroup.xml | 21 + ...iaGallerySelectSourceFilterActionGroup.xml | 22 + ...iaGallerySelectUsedInFilterActionGroup.xml | 25 + ...ncedMediaGalleryUploadImageActionGroup.xml | 24 + ...lleryVerifyImageDescriptionActionGroup.xml | 25 + ...iaGalleryVerifyImageDetailsActionGroup.xml | 37 ++ ...aGalleryVerifyImageFilenameActionGroup.xml | 25 + ...aGalleryVerifyImageKeywordsActionGroup.xml | 25 + ...ediaGalleryVerifyImageTitleActionGroup.xml | 25 + ...ediaGalleryViewImageDetailsActionGroup.xml | 20 + ...diaGalleryApplySelectFilterActionGroup.xml | 23 + ...diaGalleryApplyUsedInFilterActionGroup.xml | 23 + ...tCategoryNameInCategoryGridActionGroup.xml | 21 + ...eryAssertFolderDoesNotExistActionGroup.xml | 18 + ...ediaGalleryAssertFolderNameActionGroup.xml | 17 + ...diaGalleryAssertImageInGridActionGroup.xml | 21 + ...sertImageNotExistsInTheGridActionGroup.xml | 21 + ...ediaGalleryClickAddSelectedActionGroup.xml | 16 + ...ediaGalleryClickImageInGridActionGroup.xml | 21 + ...alleryClickOkButtonTinyMce4ActionGroup.xml | 20 + ...MediaGalleryCreateNewFolderActionGroup.xml | 18 + ...aGalleryEditAssetAddKeywordActionGroup.xml | 22 + ...nMediaGalleryEnhancedEnableActionGroup.xml | 24 + ...minMediaGalleryFolderDeleteActionGroup.xml | 17 + ...minMediaGalleryFolderSelectActionGroup.xml | 19 + ...dminMediaGalleryImageDeleteActionGroup.xml | 21 + ...diaGalleryOpenNewFolderFormActionGroup.xml | 15 + ...ryFromCategoryImageUploaderActionGroup.xml | 20 + ...ediaGalleryFromPageNoEditorActionGroup.xml | 20 + ...ediaGalleryFromTinyMce4IconActionGroup.xml | 22 + ...nOpenStandaloneMediaGalleryActionGroup.xml | 15 + ...cedMediaGalleryImageDeletedActionGroup.xml | 20 + ...sertImageAddedToPageContentActionGroup.xml | 24 + ...butesOnEnhancedMediaGalleryActionGroup.xml | 36 ++ ...lleryAdminDataGridByKeywordActionGroup.xml | 19 + ...tImageInCategoryDescriptionActionGroup.xml | 26 + .../AdminEnhancedMediaGalleryImageData.xml | 43 ++ .../Mftf/Data/AdminMediaGalleryFolderData.xml | 17 + .../Test/Mftf/Data/AdobeStockConfigData.xml | 18 + .../Page/AdminStandaloneMediaGalleryPage.xml | 12 + .../Mftf/Section/AdminConfigSystemSection.xml | 15 + ...dminEnhancedMediaGalleryActionsSection.xml | 17 + ...EnhancedMediaGalleryDeleteModalSection.xml | 14 + ...EnhancedMediaGalleryEditDetailsSection.xml | 20 + ...dminEnhancedMediaGalleryFiltersSection.xml | 29 ++ ...nhancedMediaGalleryImageActionsSection.xml | 17 + ...cedMediaGalleryImageDescriptionSection.xml | 15 + ...nEnhancedMediaGalleryMassActionSection.xml | 16 + ...EnhancedMediaGalleryViewDetailsSection.xml | 24 + .../AdminMediaGalleryFolderSection.xml | 22 + .../AdminMediaGalleryHeaderButtonsSection.xml | 15 + .../Test/Mftf/Suite/MediaGalleryUiSuite.xml | 29 ++ ...ncedMediaGalleryDeleteImagesInBulkTest.xml | 50 ++ ...hancedMediaGalleryDuplicatedImagesTest.xml | 55 ++ ...ediaGalleryUploadImageWithMetadataTest.xml | 70 +++ ...ancedMediaGalleryVerifyAssetFilterTest.xml | 80 +++ ...iaGalleryVerifyNotUsedOptionFilterTest.xml | 79 +++ ...ncedMediaGalleryVerifyUsedInFilterTest.xml | 83 +++ ...yAddCategoryImageFromTwoComponentsTest.xml | 78 +++ .../AdminMediaGalleryAddCategoryImageTest.xml | 55 ++ ...minMediaGalleryAddFromImageDetailsTest.xml | 41 ++ ...dminMediaGalleryCreateDeleteFolderTest.xml | 57 +++ ...MediaGalleryDeleteImageContextMenuTest.xml | 33 ++ .../AdminMediaGalleryDeleteImageFileTest.xml | 41 ++ ...GalleryDeleteImageWithWarningPopupTest.xml | 56 ++ ...nMediaGalleryDisabledContentFilterTest.xml | 65 +++ .../AdminMediaGalleryEditImageDetailsTest.xml | 48 ++ ...inMediaGalleryEnabledContentFilterTest.xml | 57 +++ ...inMediaGalleryFilterImagesBySourceTest.xml | 53 ++ .../AdminMediaGallerySaveFiltersStateTest.xml | 45 ++ ...ediaGalleryStoreViewCategoryFilterTest.xml | 62 +++ ...MediaGalleryStoreViewContentFilterTest.xml | 61 +++ ...minMediaGalleryUploadCategoryImageTest.xml | 43 ++ ...iaGalleryVerifyImageGridAttributesTest.xml | 37 ++ ...MediaGalleryViewDetailsDeleteImageTest.xml | 37 ++ .../AdminMediaGalleryViewDetailsEditTest.xml | 46 ++ .../Test/AdminMediaGalleryViewDetailsTest.xml | 40 ++ ...loneMediaGalleryCreateDeleteFolderTest.xml | 56 ++ ...daloneMediaGalleryEditImageDetailsTest.xml | 47 ++ ...ndaloneMediaGalleryViewDetailsEditTest.xml | 55 ++ ...nStandaloneMediaGalleryViewDetailsTest.xml | 40 ++ .../Test/Unit/Model/ConfigTest.php | 64 +++ .../Test/Unit/Model/UploadImageTest.php | 136 +++++ .../Test/Unit/_files/subdir/test_img2.jpeg | Bin 0 -> 58077 bytes .../Test/Unit/_files/test_img1.jpeg | Bin 0 -> 58077 bytes .../Ui/Component/DirectoriesTree.php | 60 +++ .../Ui/Component/ImageUploader.php | 71 +++ .../Ui/Component/ImageUploaderStandAlone.php | 36 ++ .../Listing/Columns/SourceIconProvider.php | 106 ++++ .../Ui/Component/Listing/Columns/Url.php | 119 +++++ .../Ui/Component/Listing/Filters/Asset.php | 102 ++++ .../Listing/Filters/Options/Status.php | 27 + .../Listing/Filters/Options/Store.php | 42 ++ .../Listing/Filters/Options/UsedIn.php | 37 ++ .../Ui/Component/Listing/Provider.php | 88 ++++ app/code/Magento/MediaGalleryUi/composer.json | 30 ++ .../MediaGalleryUi/etc/adminhtml/di.xml | 70 +++ .../MediaGalleryUi/etc/adminhtml/menu.xml | 13 + .../MediaGalleryUi/etc/adminhtml/routes.xml | 15 + .../MediaGalleryUi/etc/adminhtml/system.xml | 21 + .../Magento/MediaGalleryUi/etc/config.xml | 16 + app/code/Magento/MediaGalleryUi/etc/di.xml | 59 +++ .../Magento/MediaGalleryUi/etc/module.xml | 14 + .../Magento/MediaGalleryUi/i18n/en_US.csv | 8 + .../Magento/MediaGalleryUi/registration.php | 10 + .../layout/media_gallery_index_index.xml | 32 ++ .../layout/media_gallery_media_index.xml | 26 + .../view/adminhtml/templates/container.phtml | 29 ++ .../adminhtml/templates/image_details.phtml | 109 ++++ .../templates/image_details_standalone.phtml | 101 ++++ .../templates/image_edit_details.phtml | 97 ++++ .../image_edit_details_standalone.phtml | 99 ++++ .../ui_component/cms_block_listing.xml | 38 ++ .../ui_component/cms_page_listing.xml | 38 ++ .../ui_component/media_gallery_listing.xml | 393 ++++++++++++++ .../ui_component/product_listing.xml | 38 ++ .../standalone_media_gallery_listing.xml | 380 ++++++++++++++ .../adminhtml/web/css/source/_module.less | 478 ++++++++++++++++++ .../view/adminhtml/web/images/3-dots.png | Bin 0 -> 3533 bytes .../view/adminhtml/web/images/Astock.png | Bin 0 -> 359 bytes .../view/adminhtml/web/images/d.png | Bin 0 -> 12159 bytes .../deleteImageWithDetailConfirmation.js | 75 +++ .../adminhtml/web/js/action/deleteImages.js | 130 +++++ .../adminhtml/web/js/action/getDetails.js | 60 +++ .../adminhtml/web/js/action/saveDetails.js | 56 ++ .../view/adminhtml/web/js/container.js | 34 ++ .../js/directory/actions/createDirectory.js | 61 +++ .../js/directory/actions/deleteDirectory.js | 60 +++ .../adminhtml/web/js/directory/directories.js | 186 +++++++ .../web/js/directory/directoryTree.js | 477 +++++++++++++++++ .../adminhtml/web/js/grid/columns/image.js | 288 +++++++++++ .../web/js/grid/columns/image/actions.js | 109 ++++ .../grid/columns/image/insertImageAction.js | 131 +++++ .../view/adminhtml/web/js/grid/masonry.js | 49 ++ .../web/js/grid/massaction/massactionView.js | 147 ++++++ .../web/js/grid/massaction/massactions.js | 151 ++++++ .../view/adminhtml/web/js/grid/messages.js | 77 +++ .../view/adminhtml/web/js/grid/sortBy.js | 77 +++ .../view/adminhtml/web/js/image-uploader.js | 245 +++++++++ .../adminhtml/web/js/image/image-actions.js | 130 +++++ .../adminhtml/web/js/image/image-details.js | 174 +++++++ .../view/adminhtml/web/js/image/image-edit.js | 228 +++++++++ .../validation/validate-image-description.js | 19 + .../js/validation/validate-image-keyword.js | 19 + .../web/js/validation/validate-image-title.js | 19 + .../web/template/grid/columns/image.html | 45 ++ .../template/grid/columns/image/actions.html | 15 + .../grid/directories/directoryTree.html | 10 + .../web/template/grid/filter/checkbox.html | 24 + .../grid/filters/elements/ui-select.html | 133 +++++ .../grid/massactions/cancelButton.html | 10 + .../web/template/grid/massactions/count.html | 9 + .../adminhtml/web/template/grid/messages.html | 15 + .../adminhtml/web/template/grid/toolbar.html | 32 ++ .../web/template/image-uploader.html | 17 + .../adminhtml/web/template/image/actions.html | 12 + .../web/template/image/image-details.html | 65 +++ .../web/template/image/image-edit.html | 74 +++ .../MediaGalleryUiApi/Api/ConfigInterface.php | 22 + .../Magento/MediaGalleryUiApi/LICENSE.txt | 48 ++ .../Magento/MediaGalleryUiApi/LICENSE_AFL.txt | 48 ++ app/code/Magento/MediaGalleryUiApi/README.md | 13 + .../Magento/MediaGalleryUiApi/composer.json | 21 + .../Magento/MediaGalleryUiApi/etc/module.xml | 10 + .../MediaGalleryUiApi/registration.php | 10 + composer.json | 14 + composer.lock | 2 +- 427 files changed, 23252 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php create mode 100644 app/code/Magento/MediaContentSynchronization/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronization/Model/Consume.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/Publish.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php create mode 100644 app/code/Magento/MediaContentSynchronization/Model/Synchronize.php create mode 100644 app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php create mode 100644 app/code/Magento/MediaContentSynchronization/README.md create mode 100644 app/code/Magento/MediaContentSynchronization/composer.json create mode 100644 app/code/Magento/MediaContentSynchronization/etc/communication.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml create mode 100644 app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml create mode 100644 app/code/Magento/MediaContentSynchronization/registration.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizerInterface.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php create mode 100644 app/code/Magento/MediaContentSynchronizationApi/README.md create mode 100644 app/code/Magento/MediaContentSynchronizationApi/composer.json create mode 100644 app/code/Magento/MediaContentSynchronizationApi/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronizationApi/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronizationApi/registration.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/README.md create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/composer.json create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCatalog/registration.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/README.md create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php create mode 100644 app/code/Magento/MediaContentSynchronizationCms/composer.json create mode 100644 app/code/Magento/MediaContentSynchronizationCms/etc/di.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCms/etc/module.xml create mode 100644 app/code/Magento/MediaContentSynchronizationCms/registration.php create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/README.md create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/composer.json create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryCatalogIntegration/registration.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/README.md create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/composer.json create mode 100644 app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/registration.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less create mode 100644 app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryCmsUi/README.md create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/composer.json create mode 100644 app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/registration.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryIntegration/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php create mode 100644 app/code/Magento/MediaGalleryIntegration/README.md create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php create mode 100644 app/code/Magento/MediaGalleryIntegration/composer.json create mode 100644 app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryIntegration/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryIntegration/registration.php create mode 100644 app/code/Magento/MediaGalleryMetadata/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/File.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Metadata.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Segment.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php create mode 100644 app/code/Magento/MediaGalleryMetadata/README.md create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png create mode 100644 app/code/Magento/MediaGalleryMetadata/composer.json create mode 100644 app/code/Magento/MediaGalleryMetadata/etc/default.xmp create mode 100644 app/code/Magento/MediaGalleryMetadata/etc/di.xml create mode 100644 app/code/Magento/MediaGalleryMetadata/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryMetadata/registration.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php create mode 100644 app/code/Magento/MediaGalleryMetadataApi/README.md create mode 100644 app/code/Magento/MediaGalleryMetadataApi/composer.json create mode 100644 app/code/Magento/MediaGalleryMetadataApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryMetadataApi/registration.php create mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryRenditions/README.md create mode 100644 app/code/Magento/MediaGalleryRenditions/composer.json create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/config.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryRenditions/registration.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/Model/Config.php create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/README.md create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/composer.json create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryRenditionsApi/registration.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php create mode 100644 app/code/Magento/MediaGalleryUi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/Config.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/DeleteImage.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/Directories/FolderTree.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/UploadImage.php create mode 100644 app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php create mode 100644 app/code/Magento/MediaGalleryUi/README.md create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg create mode 100644 app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php create mode 100644 app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php create mode 100644 app/code/Magento/MediaGalleryUi/composer.json create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/config.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/di.xml create mode 100644 app/code/Magento/MediaGalleryUi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryUi/i18n/en_US.csv create mode 100644 app/code/Magento/MediaGalleryUi/registration.php create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/cancelButton.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html create mode 100644 app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html create mode 100644 app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php create mode 100644 app/code/Magento/MediaGalleryUiApi/LICENSE.txt create mode 100644 app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt create mode 100644 app/code/Magento/MediaGalleryUiApi/README.md create mode 100644 app/code/Magento/MediaGalleryUiApi/composer.json create mode 100644 app/code/Magento/MediaGalleryUiApi/etc/module.xml create mode 100644 app/code/Magento/MediaGalleryUiApi/registration.php 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 0000000000000000000000000000000000000000..14cc6026b5950c8b2546c198467dc59dc31c1724 GIT binary patch literal 7951 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 F{{RBl3$y?L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..144a56dac2d3e748d47cf56e0fdde0bab2f49def GIT binary patch literal 19416 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..129c49a1b7e64148b223123349fc21f2dc1393c8 GIT binary patch literal 47596 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 HP00TMZ+Z(Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cee7bff38a6c62b9bb8d59b682b10567c6c696da GIT binary patch literal 19336 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 Y_Wa?0f${ujYsB!kdAI-N$2$K10F^uY0RR91 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7e81891ebc0eee3059e8e5455158993a0aa839fe GIT binary patch literal 55268 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 FzX0#`%BcVV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..70574d70b609e0a180bbf680447f6e970f484b43 GIT binary patch literal 12704 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d7dba35fede7547ce742ea33ce62cc71d5f825f GIT binary patch literal 19884 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9b4821c1c4e5d755df65d09450bcb2b5c925e812 GIT binary patch literal 47894 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a07b6abe788e83cc336eb8b198556c46e7ec911 GIT binary patch literal 22795 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 L<aM`$-mNzQDffZL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..966520f0d01124eec138a9e29298dbe8e2fea5ab GIT binary patch literal 57535 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 V<L%D>xo;2f)6+51CZ4mo`(IOuD**ri literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5244f8dc420e188df576181f313075c73b176d13 GIT binary patch literal 58077 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 A9smFU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5244f8dc420e188df576181f313075c73b176d13 GIT binary patch literal 58077 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 A9smFU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..601ba415f2446038f3e34cda7cc503329e227fa7 GIT binary patch literal 3533 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 Hu0mjfJiWe5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..db5cda9c5512b899cf82293dfb5bf1ba7da831cd GIT binary patch literal 359 zcmV-t0hs=YP)<h;3K|Lk000e1NJLTq001Wd000~a0{{R3)xcJ10000#P)t-se}8}d z{r&3d>Zquwii(Qm<>lAc*Z=?j`uh6L&d#5opUKI|d3kx?-`{d_a`W@^l9H0RxVV~{ zn#9D!($dm3gMRS<0084jL_t(I%e~Xtj)O1^1yDPy19mp}|8K3KQq@YBjx?jWFAz9N z!UZ57LAuK!;AE|=W{SLAj+4O%hxF!_#TnT?ozK_7@-s)}H}fI2Ba{)JbzD2`X9nm# zW{SJmt_?o=2_;L1&2|#li=<UBg|MD0U%I_^!RMHHj%c~W>|Mj-q*m^`<(%gVBT{Y~ z=&_l-I1ef}(w*wV+`JDI4+_6Jy^(rd@ZM1)#5U@rdzbnqZtd)KT^Nn*UaE#?)C;wq zGhX*HIzS&zcaSDb`M2-y59hOf^7YfrVr0BKw}6>`x(8}U9pcx(xCa0L002ovPDHLk FV1nU$w3q+@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6516e915624c3348d83b2c4bb35a1e14bb2e20cb GIT binary patch literal 12159 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 Ba*zN3 literal 0 HcmV?d00001 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",