From b4cdaf75157a68fd9bd71be75fe0e0955a56a068 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko Date: Sun, 6 Oct 2019 21:45:34 +0100 Subject: [PATCH 1/2] Decomposed image-preview component --- AdobeStockImage/etc/acl.xml | 22 - AdobeStockImage/etc/adminhtml/routes.xml | 14 - .../Block/Adminhtml/Panel.php | 84 --- .../Listing/Columns/ImagePreview.php | 62 ++ .../layout/cms_wysiwyg_images_index.xml | 2 +- .../view/adminhtml/templates/panel.phtml | 7 +- .../adobe_stock_images_listing.xml | 2 +- .../components/grid/column/image-preview.js | 637 +++--------------- .../components/grid/column/preview/actions.js | 242 +++++++ .../grid/column/preview/keywords.js | 94 +++ .../components/grid/column/preview/related.js | 228 +++++++ .../view/adminhtml/web/js/config.js | 16 - .../view/adminhtml/web/panel.js | 11 +- .../template/grid/column/image-preview.html | 107 +-- .../template/grid/column/preview/actions.html | 29 + .../grid/column/preview/keywords.html | 19 + .../template/grid/column/preview/related.html | 64 ++ .../components/grid/column/image-preview.js | 72 +- .../web/js/components/grid/column/image.js | 1 + .../web/js/components/grid/column/overlay.js | 1 + .../web/js/components/grid/masonry.js | 8 +- 21 files changed, 909 insertions(+), 813 deletions(-) delete mode 100644 AdobeStockImage/etc/acl.xml delete mode 100644 AdobeStockImage/etc/adminhtml/routes.xml delete mode 100644 AdobeStockImageAdminUi/Block/Adminhtml/Panel.php create mode 100644 AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php create mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/actions.js create mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/keywords.js create mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/related.js delete mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/js/config.js create mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/actions.html create mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/keywords.html create mode 100644 AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/related.html diff --git a/AdobeStockImage/etc/acl.xml b/AdobeStockImage/etc/acl.xml deleted file mode 100644 index f8959a15e6b9..000000000000 --- a/AdobeStockImage/etc/acl.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/AdobeStockImage/etc/adminhtml/routes.xml b/AdobeStockImage/etc/adminhtml/routes.xml deleted file mode 100644 index 9e7bae0c0e58..000000000000 --- a/AdobeStockImage/etc/adminhtml/routes.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/AdobeStockImageAdminUi/Block/Adminhtml/Panel.php b/AdobeStockImageAdminUi/Block/Adminhtml/Panel.php deleted file mode 100644 index fd084477f8b7..000000000000 --- a/AdobeStockImageAdminUi/Block/Adminhtml/Panel.php +++ /dev/null @@ -1,84 +0,0 @@ -client = $client; - $this->authorized = $authorized; - $this->userQuotaFactory = $userQuotaFactory; - parent::__construct($context, $data); - } - - /** - * Get user quota - * - * @return UserQuotaInterface - */ - public function getUserQuota(): UserQuotaInterface - { - if ($this->authorized->execute()) { - return $this->client->getQuota(); - } else { - $userQuota = $this->userQuotaFactory->create(); - $userQuota->setImages(0); - $userQuota->setCredits(0); - return $userQuota; - } - } - - /** - * Get URL for buying more credits - * - * @return string - */ - public function getBuyCreditsUrl(): string - { - return 'https://stock.adobe.com/'; - } -} diff --git a/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php b/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php new file mode 100644 index 000000000000..01404486373b --- /dev/null +++ b/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php @@ -0,0 +1,62 @@ +url = $url; + } + /** + * @inheritdoc + */ + public function prepare() + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'downloadImagePreviewUrl' => $this->url->getUrl('adobe_stock/preview/download'), + 'licenseAndDownloadUrl' => $this->url->getUrl('adobe_stock/license/license'), + 'confirmationUrl' => $this->url->getUrl('adobe_stock/license/confirmation'), + 'relatedImagesUrl' => $this->url->getUrl('adobe_stock/preview/relatedimages'), + 'buyCreditsUrl' => 'https://stock.adobe.com/' + ] + ) + ); + } +} \ No newline at end of file diff --git a/AdobeStockImageAdminUi/view/adminhtml/layout/cms_wysiwyg_images_index.xml b/AdobeStockImageAdminUi/view/adminhtml/layout/cms_wysiwyg_images_index.xml index a63739f67f06..4c7aa0231869 100644 --- a/AdobeStockImageAdminUi/view/adminhtml/layout/cms_wysiwyg_images_index.xml +++ b/AdobeStockImageAdminUi/view/adminhtml/layout/cms_wysiwyg_images_index.xml @@ -7,7 +7,7 @@ --> - + diff --git a/AdobeStockImageAdminUi/view/adminhtml/templates/panel.phtml b/AdobeStockImageAdminUi/view/adminhtml/templates/panel.phtml index c4e7e9af1962..76fd1c057e90 100644 --- a/AdobeStockImageAdminUi/view/adminhtml/templates/panel.phtml +++ b/AdobeStockImageAdminUi/view/adminhtml/templates/panel.phtml @@ -23,12 +23,7 @@ "stock_panel": { "component": "Magento_AdobeStockImageAdminUi/panel", "containerId": "#adobe-stock-images-search-modal", - "masonryComponentPath": "adobe_stock_images_listing.adobe_stock_images_listing.adobe_stock_images_columns", - "downloadPreviewUrl": "getUrl('adobe_stock/preview/download') ?>", - "licenseAndDownloadUrl": "getUrl('adobe_stock/license/license') ?>", - "confirmationUrl": "getUrl('adobe_stock/license/confirmation') ?>", - "relatedImagesUrl": "getUrl('adobe_stock/preview/relatedimages') ?>", - "buyCreditsUrl": "getBuyCreditsUrl() ?>" + "masonryComponentPath": "adobe_stock_images_listing.adobe_stock_images_listing.adobe_stock_images_columns" } } } diff --git a/AdobeStockImageAdminUi/view/adminhtml/ui_component/adobe_stock_images_listing.xml b/AdobeStockImageAdminUi/view/adminhtml/ui_component/adobe_stock_images_listing.xml index 6128b37fa523..bd1e7327763e 100644 --- a/AdobeStockImageAdminUi/view/adminhtml/ui_component/adobe_stock_images_listing.xml +++ b/AdobeStockImageAdminUi/view/adminhtml/ui_component/adobe_stock_images_listing.xml @@ -187,7 +187,7 @@ false - + Magento_AdobeStockImageAdminUi/grid/column/image-preview diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/image-preview.js b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/image-preview.js index c0d58a0e287c..640e89fca405 100644 --- a/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/image-preview.js +++ b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/image-preview.js @@ -3,90 +3,96 @@ * See COPYING.txt for license details. */ define([ - 'underscore', 'jquery', - 'knockout', - 'mage/translate', - 'Magento_AdobeUi/js/components/grid/column/image-preview', - 'Magento_AdobeStockImageAdminUi/js/model/messages', - 'Magento_AdobeStockImageAdminUi/js/media-gallery', - 'Magento_Ui/js/modal/confirm', - 'Magento_Ui/js/modal/prompt', - 'text!Magento_AdobeStockImageAdminUi/template/modal/adobe-modal-prompt-content.html', - 'Magento_AdobeStockImageAdminUi/js/config', - 'mage/backend/tabs' -], function (_, $, ko, translate, imagePreview, messages, mediaGallery, confirmation, prompt, adobePromptContentTmpl, config) { + 'uiLayout', + 'Magento_AdobeUi/js/components/grid/column/image-preview' +], function ($, layout, imagePreview) { 'use strict'; return imagePreview.extend({ defaults: { - chipsProvider: 'componentType = filtersChips, ns = ${ $.ns }', - searchChipsProvider: 'componentType = keyword_search, ns = ${ $.ns }', - filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', - loginProvider: 'name = adobe-login, ns = adobe-login', - inputValue: '', - chipInputValue: '', - serieFilterValue: '', - modelFilterValue: '', - defaultKeywordsLimit: 5, - keywordsLimit: 5, - canViewMoreKeywords: true, - saveAvailable: true, - searchValue: null, - messageDelay: 5, - displayedRecord: {}, - selectedRelatedType: null, - previewStyles: {}, - statefull: { - visible: true, - sorting: true, - lastOpenedImage: true, - serieFilterValue: true, - modelFilterValue: true - }, - tracks: { - lastOpenedImage: true - }, + downloadImagePreviewUrl: 'adobe_stock/preview/download', + licenseAndDownloadUrl: 'adobe_stock/license/license', + confirmationUrl: 'adobe_stock/license/confirmation', + relatedImagesUrl: 'adobe_stock/preview/relatedimages', + buyCreditsUrl: 'https://stock.adobe.com/', + mediaGallerySelector: '.media-gallery-modal:has(#search_adobe_stock)', + adobeStockModalSelector: '#adobe-stock-images-search-modal', modules: { - thumbnailComponent: '${ $.parentName }.thumbnail_url', - chips: '${ $.chipsProvider }', - searchChips: '${ $.searchChipsProvider }', - filterChips: '${ $.filterChipsProvider }', - login: '${ $.loginProvider }', - }, - listens: { - '${ $.provider }:params.filters': 'hide', - '${ $.provider }:params.search': 'hide' + keywords: '${ $.name }_keywords', + related: '${ $.name }_related', + actions: '${ $.name }_actions' }, - exports: { - inputValue: '${ $.provider }:params.search', - serieFilterValue: '${ $.provider }:params.filters.serie_id', - modelFilterValue: '${ $.provider }:params.filters.model_id', - chipInputValue: '${ $.searchChipsProvider }:value' - } + viewConfig: [ + { + component: 'Magento_AdobeStockImageAdminUi/js/components/grid/column/preview/keywords', + name: '${ $.name }_keywords' + }, + { + component: 'Magento_AdobeStockImageAdminUi/js/components/grid/column/preview/related', + name: '${ $.name }_related' + }, + { + component: 'Magento_AdobeStockImageAdminUi/js/components/grid/column/preview/actions', + name: '${ $.name }_actions' + } + ] + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super().initView(); + + return this; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; }, /** + * Get previous button disabled + * * @param {Object} record - * @private + * + * @return {Boolean} */ - _initRecord: function (record) { - if (!record.model || !record.series) { - record.series = ko.observable([]); - record.model = ko.observable([]); - } + getPreviousButtonDisabled: function (record) { + return this.related().getPreviousButtonDisabled(record); + }, + + /** + * Get next button disabled + * + * @param {Object} record + * + * @return {Boolean} + */ + getNextButtonDisabled: function (record) { + return this.related().getNextButtonDisabled(record); }, /** * @inheritdoc */ next: function (record) { - if (this.selectedRelatedType()) { - this.nextRelated(record); + if (this.related().selectedRelatedType()) { + this.related().nextRelated(record); return; } - this.hideAllKeywords(); + this.keywords().hideAllKeywords(); this._super(record); }, @@ -94,12 +100,12 @@ define([ * @inheritdoc */ prev: function (record) { - if (this.selectedRelatedType()) { - this.prevRelated(record); + if (this.related().selectedRelatedType()) { + this.related().prevRelated(record); return; } - this.hideAllKeywords(); + this.keywords().hideAllKeywords(); this._super(record); }, @@ -107,39 +113,23 @@ define([ * @inheritdoc */ show: function (record) { - this.selectedRelatedType(null); - this._initRecord(record); - this.hideAllKeywords(); + this.related().selectedRelatedType(null); + this.related().initRecord(record); + this.keywords().hideAllKeywords(); this.displayedRecord(record); this._super(record); this.loadRelatedImages(record); - this._updateHeight(); }, /** - * Init observable variables - * @return {Object} + * Show related image data in the preview section + * + * @param {Object} record */ - initObservable: function () { - this._super() - .observe([ - 'visibility', - 'height', - 'inputValue', - 'chipInputValue', - 'serieFilterValue', - 'modelFilterValue', - 'displayedRecord', - 'keywordsLimit', - 'canViewMoreKeywords', - 'selectedRelatedType', - 'previewStyles' - ]); - this.height.subscribe(function () { - this.thumbnailComponent().previewHeight(this.height()); - }, this); - - return this; + showRelated: function (record) { + this.keywords().hideAllKeywords(); + this.displayedRecord(record); + this._updateHeight(); }, /** @@ -155,7 +145,7 @@ define([ } $.ajax({ type: 'GET', - url: config.relatedImagesUrl, + url: this.relatedImagesUrl, dataType: 'json', showLoader: true, data: { @@ -193,465 +183,6 @@ define([ value: this.displayedRecord().id } ]; - }, - - /** - * Returns series to display under the image - * - * @param {Object} record - * @returns {*[]} - */ - getSeries: function (record) { - return record.series; - }, - - /** - * Returns model to display under the image - * - * @param {Object} record - * @returns {*[]} - */ - getModel: function (record) { - return record.model; - }, - - /** - * Filter images from serie_id - * - * @param {Object} record - */ - seeMoreFromSeries: function(record) { - this.serieFilterValue(record.id); - this.filterChips().set('applied', {'serie_id' : record.id.toString()}) - }, - - /** - * Filter images from serie_id - * - * @param {Object} record - */ - seeMoreFromModel: function(record) { - this.modelFilterValue(record.id); - this.filterChips().set('applied', {'model_id' : record.id.toString()}) - }, - - /** - * Returns keywords to display under the attributes image - * - * @returns {*[]} - */ - getKeywords: function () { - return this.displayedRecord().keywords; - }, - - /** - * Returns keywords limit to show no of keywords - */ - getKeywordsLimit: function () { - return this.keywordsLimit(); - }, - - /** - * Show all the related keywords - */ - viewAllKeywords: function () { - this.keywordsLimit(this.displayedRecord().keywords.length); - this.canViewMoreKeywords(false); - this._updateHeight(); - }, - - /** - * Hide all the related keywords - */ - hideAllKeywords: function () { - this.keywordsLimit(this.defaultKeywordsLimit); - this.canViewMoreKeywords(true); - }, - - /** - * Check if view all button is visible or not - * - * @returns {boolean} - */ - canViewMoreKeywords: function () { - return this.canViewMoreKeywords(); - }, - - /** - * Drop all filters and initiate search on keyword click event - */ - searchByKeyWord: function (keyword) { - _.invoke(this.chips().elems(), 'clear'); - this.inputValue(keyword); - this.chipInputValue(keyword); - }, - - /** - * Returns is_downloaded flag as observable for given record - * - * @returns {observable} - */ - isDownloaded: function () { - return this.displayedRecord().is_downloaded; - }, - - /** - * Get styles for preview - * - * @returns {Object} - */ - getStyles: function () { - this.previewStyles({'margin-top': '-' + this.height()}); - return this.previewStyles(); - }, - - /** - * Scroll to preview window - */ - scrollToPreview: function () { - $(this.previewImageSelector).get(0).scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "nearest" - }); - }, - - /** - * Locate downloaded image in media browser - */ - locate: function () { - $(config.adobeStockModalSelector).trigger('closeModal'); - mediaGallery.locate(this.displayedRecord().path); - }, - - /** - * Save preview - */ - savePreview: function () { - prompt({ - title: 'Save Preview', - content: 'File Name', - value: this.generateImageName(this.displayedRecord()), - imageExtension: this.getImageExtension(this.displayedRecord()), - promptContentTmpl: adobePromptContentTmpl, - modalClass: 'adobe-stock-save-preview-prompt', - validation: true, - promptField: '[data-role="promptField"]', - validationRules: ['required-entry'], - attributesForm: { - novalidate: 'novalidate', - action: '', - onkeydown: 'return event.key != \'Enter\';' - }, - attributesField: { - name: 'name', - 'data-validate': '{required:true}', - maxlength: '128' - }, - context: this, - actions: { - confirm: function (fileName) { - this.save(this.displayedRecord(), fileName, config.downloadPreviewUrl); - }.bind(this) - }, - buttons: [{ - text: $.mage.__('Cancel'), - class: 'action-secondary action-dismiss' - }, { - text: $.mage.__('Confirm'), - class: 'action-primary action-accept' - }] - }); - }, - - /** - * Save record as image - * - * @param {Object} record - * @param {String} fileName - * @param {String} actionURI - */ - save: function (record, fileName, actionURI) { - var mediaBrowser = $(config.mediaGallerySelector).data('mageMediabrowser'), - destinationPath = (mediaBrowser.activeNode.path || '') + '/' + fileName + '.' + this.getImageExtension(record); - - $.ajax({ - type: 'POST', - url: actionURI, - dataType: 'json', - showLoader: true, - data: { - 'media_id': record.id, - 'destination_path': destinationPath - }, - context: this, - success: function () { - var displayedRecord = this.displayedRecord(); - displayedRecord.is_downloaded = 1; - displayedRecord.path = destinationPath; - this.displayedRecord(displayedRecord); - $(config.adobeStockModalSelector).trigger('closeModal'); - mediaBrowser.reload(true); - }, - error: function (response) { - messages.add('error', response.message); - messages.scheduleCleanup(3); - } - }); - }, - - /** - * Generate meaningful name image file - * - * @param {Object} record - * @return string - */ - generateImageName: function (record) { - var imageName = record.title.substring(0, 32).replace(/\s+/g, '-').toLowerCase(); - return imageName; - }, - - /** - * Get image file extension - * - * @param {Object} record - * @return string - */ - getImageExtension: function (record) { - var imageType = record.content_type.match(/[^/]{1,4}$/); - return imageType; - }, - - /** - * Get messages - * - * @return {Array} - */ - getMessages: function () { - return messages.get(); - }, - - /** - * License and save image - * - * @param {Object} record - */ - licenseAndSave: function (record) { - this.save(record, this.generateImageName(record), config.licenseAndDownloadUrl); - }, - - /** - * Shows license confirmation popup with information about current license quota - * - * @param {Object} record - */ - showLicenseConfirmation: function (record) { - var licenseAndSave = this.licenseAndSave.bind(this); - $.ajax( - { - type: 'POST', - url: config.confirmationUrl, - dataType: 'json', - data: { - 'media_id': record.id - }, - context: this, - showLoader: true, - - success: function (response) { - var confirmationContent = $.mage.__('License "' + record.title + '"'), - quotaMessage = response.result.message, - canPurchase = response.result.canLicense; - confirmation({ - title: $.mage.__('License Adobe Stock Image?'), - content: confirmationContent + '

' + quotaMessage + '

', - actions: { - confirm: function () { - if (canPurchase) { - licenseAndSave(record); - } else { - window.open(config.buyCreditsUrl); - } - } - }, - buttons: [{ - text: $.mage.__('Cancel'), - class: 'action-secondary action-dismiss', - click: function () { - this.closeModal(); - } - }, { - text: canPurchase ? $.mage.__('OK') : $.mage.__('Buy Credits'), - class: 'action-primary action-accept', - click: function () { - this.closeModal(); - this.options.actions.confirm(); - } - }] - }) - }, - - error: function (response) { - messages.add('error', response.responseJSON.message); - messages.scheduleCleanup(3); - } - } - ); - }, - - /** - * Process of license - * - * @param {Object} record - */ - licenseProcess: function () { - this.login().login() - .then(function () { - this.showLicenseConfirmation(this.displayedRecord()); - }.bind(this)) - .catch(function (error) { - messages.add('error', error.message); - }) - .finally((function () { - messages.scheduleCleanup(this.messageDelay); - }).bind(this)); - }, - - /** - * Show related image data in the preview section - * - * @param {Object} record - */ - showRelated: function (record) { - this.hideAllKeywords(); - this.displayedRecord(record); - this._updateHeight(); - }, - - /** - * Next related image preview - * - * @param {Object} record - */ - nextRelated: function (record) { - var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), - nextRelatedIndex = _.findLastIndex(relatedList, {id: this.displayedRecord().id}) + 1, - nextRelated = relatedList[nextRelatedIndex]; - - if (typeof nextRelated === 'undefined') { - return; - } - - this.switchImagePreviewToRelatedImage(nextRelated, record); - }, - - /** - * Previous related preview - * - * @param {Object} record - */ - prevRelated: function (record) { - var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), - prevRelatedIndex = _.findLastIndex(relatedList, {id: this.displayedRecord().id}) - 1, - prevRelated = relatedList[prevRelatedIndex]; - - if (typeof prevRelated === 'undefined') { - return; - } - - this.switchImagePreviewToRelatedImage(prevRelated, record); - }, - - /** - * Get previous button disabled - * - * @param {Object} record - * - * @return {Boolean} - */ - getPreviousButtonDisabled: function (record) { - if (!this.selectedRelatedType()) { - return false; - } - var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), - prevRelatedIndex, - prevRelated; - - prevRelatedIndex = _.findLastIndex(relatedList, {id: this.displayedRecord().id}) - 1; - prevRelated = relatedList[prevRelatedIndex]; - - if (typeof prevRelated === 'undefined') { - return true; - } - - return false; - }, - - /** - * Get next button disabled - * - * @param {Object} record - * - * @return {Boolean} - */ - getNextButtonDisabled: function (record) { - if (!this.selectedRelatedType()) { - return false; - } - var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), - nextRelatedIndex, - nextRelated; - - nextRelatedIndex = _.findLastIndex(relatedList, {id: this.displayedRecord().id}) + 1; - nextRelated = relatedList[nextRelatedIndex]; - - if (typeof nextRelated === 'undefined') { - return true; - } - - return false; - }, - - /** - * Switch image preview to related image - * - * @param {Object|null} relatedImage - * @param {Object} record - */ - switchImagePreviewToRelatedImage: function (relatedImage, record) { - if (!relatedImage) { - this.selectedRelatedType(null); - - return; - } - - if (this.displayedRecord().id === relatedImage.id) { - return; - } - - this.showRelated(relatedImage); - }, - - /** - * Switch image preview to series image - * - * @param {Object} series - * @param {Object} record - */ - switchImagePreviewToSeriesImage: function (series, record) { - this.selectedRelatedType('series'); - this.switchImagePreviewToRelatedImage(series, record); - }, - - /** - * Switch image preview to model image - * - * @param {Object} model - * @param {Object} record - */ - switchImagePreviewToModelImage: function (model, record) { - this.selectedRelatedType('model'); - this.switchImagePreviewToRelatedImage(model, record); - }, + } }); }); diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/actions.js b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/actions.js new file mode 100644 index 000000000000..494019f2060f --- /dev/null +++ b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/actions.js @@ -0,0 +1,242 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'uiComponent', + 'jquery', + 'Magento_AdobeStockImageAdminUi/js/model/messages', + 'Magento_AdobeStockImageAdminUi/js/media-gallery', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/modal/prompt', + 'text!Magento_AdobeStockImageAdminUi/template/modal/adobe-modal-prompt-content.html' +], function (Component, $, messages, mediaGallery, confirmation, prompt, adobePromptContentTmpl) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_AdobeStockImageAdminUi/grid/column/preview/actions', + loginProvider: 'name = adobe-login, ns = adobe-login', + previewProvider: 'name = adobe_stock_images_listing.adobe_stock_images_listing.adobe_stock_images_columns.preview, ns = ${ $.ns }', + mediaGallerySelector: '.media-gallery-modal:has(#search_adobe_stock)', + adobeStockModalSelector: '#adobe-stock-images-search-modal', + downloadImagePreviewUrl: 'adobe_stock/preview/download', + licenseAndDownloadUrl: 'adobe_stock/license/license', + confirmationUrl: 'adobe_stock/license/confirmation', + buyCreditsUrl: 'https://stock.adobe.com/', + messageDelay: 5, + modules: { + login: '${ $.loginProvider }', + preview: '${ $.previewProvider }' + } + }, + + /** + * Returns is_downloaded flag as observable for given record + * + * @returns {observable} + */ + isDownloaded: function () { + return this.preview().displayedRecord().is_downloaded; + }, + + /** + * Locate downloaded image in media browser + */ + locate: function () { + $(this.preview().adobeStockModalSelector).trigger('closeModal'); + mediaGallery.locate(this.preview().displayedRecord().path); + }, + + /** + * Save preview + */ + savePreview: function () { + prompt({ + title: 'Save Preview', + content: 'File Name', + value: this.generateImageName(this.preview().displayedRecord()), + imageExtension: this.getImageExtension(this.preview().displayedRecord()), + promptContentTmpl: adobePromptContentTmpl, + modalClass: 'adobe-stock-save-preview-prompt', + validation: true, + promptField: '[data-role="promptField"]', + validationRules: ['required-entry'], + attributesForm: { + novalidate: 'novalidate', + action: '', + onkeydown: 'return event.key != \'Enter\';' + }, + attributesField: { + name: 'name', + 'data-validate': '{required:true}', + maxlength: '128' + }, + context: this, + actions: { + confirm: function (fileName) { + this.save(this.preview().displayedRecord(), fileName, this.preview().downloadImagePreviewUrl); + }.bind(this) + }, + buttons: [{ + text: $.mage.__('Cancel'), + class: 'action-secondary action-dismiss' + }, { + text: $.mage.__('Confirm'), + class: 'action-primary action-accept' + }] + }); + }, + + /** + * Save record as image + * + * @param {Object} record + * @param {String} fileName + * @param {String} actionURI + */ + save: function (record, fileName, actionURI) { + var mediaBrowser = $(this.preview().mediaGallerySelector).data('mageMediabrowser'), + destinationPath = (mediaBrowser.activeNode.path || '') + '/' + fileName + '.' + this.getImageExtension(record); + + $.ajax({ + type: 'POST', + url: actionURI, + dataType: 'json', + showLoader: true, + data: { + 'media_id': record.id, + 'destination_path': destinationPath + }, + context: this, + success: function () { + var displayedRecord = this.preview().displayedRecord(); + displayedRecord.is_downloaded = 1; + displayedRecord.path = destinationPath; + this.preview().displayedRecord(displayedRecord); + $(this.preview().adobeStockModalSelector).trigger('closeModal'); + mediaBrowser.reload(true); + }, + error: function (response) { + messages.add('error', response.message); + messages.scheduleCleanup(3); + } + }); + }, + + /** + * Generate meaningful name image file + * + * @param {Object} record + * @return string + */ + generateImageName: function (record) { + var imageName = record.title.substring(0, 32).replace(/\s+/g, '-').toLowerCase(); + return imageName; + }, + + /** + * Get image file extension + * + * @param {Object} record + * @return string + */ + getImageExtension: function (record) { + var imageType = record.content_type.match(/[^/]{1,4}$/); + return imageType; + }, + + /** + * Get messages + * + * @return {Array} + */ + getMessages: function () { + return messages.get(); + }, + + /** + * License and save image + * + * @param {Object} record + */ + licenseAndSave: function (record) { + this.save(record, this.generateImageName(record), this.preview().licenseAndDownloadUrl); + }, + + /** + * Shows license confirmation popup with information about current license quota + * + * @param {Object} record + */ + showLicenseConfirmation: function (record) { + var licenseAndSave = this.licenseAndSave.bind(this); + $.ajax( + { + type: 'POST', + url: this.preview().confirmationUrl, + dataType: 'json', + data: { + 'media_id': record.id + }, + context: this, + showLoader: true, + + success: function (response) { + var confirmationContent = $.mage.__('License "' + record.title + '"'), + quotaMessage = response.result.message, + canPurchase = response.result.canLicense; + confirmation({ + title: $.mage.__('License Adobe Stock Image?'), + content: confirmationContent + '

' + quotaMessage + '

', + actions: { + confirm: function () { + if (canPurchase) { + licenseAndSave(record); + } else { + window.open(this.preview().buyCreditsUrl); + } + } + }, + buttons: [{ + text: $.mage.__('Cancel'), + class: 'action-secondary action-dismiss', + click: function () { + this.closeModal(); + } + }, { + text: canPurchase ? $.mage.__('OK') : $.mage.__('Buy Credits'), + class: 'action-primary action-accept', + click: function () { + this.closeModal(); + this.options.actions.confirm(); + } + }] + }) + }, + + error: function (response) { + messages.add('error', response.responseJSON.message); + messages.scheduleCleanup(3); + } + } + ); + }, + + /** + * Process of license + */ + licenseProcess: function () { + this.login().login() + .then(function () { + this.showLicenseConfirmation(this.preview().displayedRecord()); + }.bind(this)) + .catch(function (error) { + messages.add('error', error.message); + }) + .finally((function () { + messages.scheduleCleanup(this.messageDelay); + }).bind(this)); + } + }); +}); diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/keywords.js b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/keywords.js new file mode 100644 index 000000000000..8f89f9dc002d --- /dev/null +++ b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/keywords.js @@ -0,0 +1,94 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'uiComponent', + 'underscore' +], function (Component, _) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_AdobeStockImageAdminUi/grid/column/preview/keywords', + chipsProvider: 'componentType = filtersChips, ns = ${ $.ns }', + searchChipsProvider: 'componentType = keyword_search, ns = ${ $.ns }', + defaultKeywordsLimit: 5, + keywordsLimit: 5, + canViewMoreKeywords: true, + modules: { + searchChips: '${ $.searchChipsProvider }', + chips: '${ $.chipsProvider }' + }, + exports: { + inputValue: '${ $.provider }:params.search', + chipInputValue: '${ $.searchChipsProvider }:value' + } + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'keywordsLimit', + 'canViewMoreKeywords' + ]); + + return this; + }, + + /** + * Returns keywords to display under the attributes image + * + * @returns {*[]} + */ + getKeywords: function (record) { + return record.keywords; + }, + + /** + * Returns keywords limit to show no of keywords + */ + getKeywordsLimit: function () { + return this.keywordsLimit(); + }, + + /** + * Show all the related keywords + */ + viewAllKeywords: function (record) { + this.keywordsLimit(record.keywords.length); + this.canViewMoreKeywords(false); + this._updateHeight(); + }, + + /** + * Hide all the related keywords + */ + hideAllKeywords: function () { + this.keywordsLimit(this.defaultKeywordsLimit); + this.canViewMoreKeywords(true); + }, + + /** + * Check if view all button is visible or not + * + * @returns {boolean} + */ + canViewMoreKeywords: function () { + return this.canViewMoreKeywords(); + }, + + /** + * Drop all filters and initiate search on keyword click event + */ + searchByKeyWord: function (keyword) { + _.invoke(this.chips().elems(), 'clear'); + this.inputValue(keyword); + this.chipInputValue(keyword); + } + }); +}); diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/related.js b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/related.js new file mode 100644 index 000000000000..f4aeae2c2a8e --- /dev/null +++ b/AdobeStockImageAdminUi/view/adminhtml/web/js/components/grid/column/preview/related.js @@ -0,0 +1,228 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'uiComponent', + 'underscore', + 'jquery', + 'ko', + 'mage/backend/tabs' +], function (Component, _, $, ko) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_AdobeStockImageAdminUi/grid/column/preview/related', + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + previewProvider: 'name = adobe_stock_images_listing.adobe_stock_images_listing.adobe_stock_images_columns.preview, ns = ${ $.ns }', + serieFilterValue: '', + modelFilterValue: '', + selectedRelatedType: null, + statefull: { + serieFilterValue: true, + modelFilterValue: true + }, + modules: { + chips: '${ $.chipsProvider }', + filterChips: '${ $.filterChipsProvider }', + preview: '${ $.previewProvider }' + }, + exports: { + serieFilterValue: '${ $.provider }:params.filters.serie_id', + modelFilterValue: '${ $.provider }:params.filters.model_id' + } + }, + + /** + * @param {Object} record + */ + initRecord: function (record) { + if (!record.model || !record.series) { + record.series = ko.observable([]); + record.model = ko.observable([]); + } + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'serieFilterValue', + 'modelFilterValue', + 'selectedRelatedType' + ]); + + return this; + }, + + /** + * Returns series to display under the image + * + * @param {Object} record + * @returns {*[]} + */ + getSeries: function (record) { + return record.series; + }, + + /** + * Returns model to display under the image + * + * @param {Object} record + * @returns {*[]} + */ + getModel: function (record) { + return record.model; + }, + + /** + * Filter images from serie_id + * + * @param {Object} record + */ + seeMoreFromSeries: function(record) { + this.serieFilterValue(record.id); + this.filterChips().set('applied', {'serie_id' : record.id.toString()}) + }, + + /** + * Filter images from serie_id + * + * @param {Object} record + */ + seeMoreFromModel: function(record) { + this.modelFilterValue(record.id); + this.filterChips().set('applied', {'model_id' : record.id.toString()}) + }, + + /** + * Next related image preview + * + * @param {Object} record + */ + nextRelated: function (record) { + var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), + nextRelatedIndex = _.findLastIndex(relatedList, {id: this.preview().displayedRecord().id}) + 1, + nextRelated = relatedList[nextRelatedIndex]; + + if (typeof nextRelated === 'undefined') { + return; + } + + this.switchImagePreviewToRelatedImage(nextRelated, record); + }, + + /** + * Previous related preview + * + * @param {Object} record + */ + prevRelated: function (record) { + var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), + prevRelatedIndex = _.findLastIndex(relatedList, {id: this.preview().displayedRecord().id}) - 1, + prevRelated = relatedList[prevRelatedIndex]; + + if (typeof prevRelated === 'undefined') { + return; + } + + this.switchImagePreviewToRelatedImage(prevRelated, record); + }, + + /** + * Get previous button disabled + * + * @param {Object} record + * + * @return {Boolean} + */ + getPreviousButtonDisabled: function (record) { + if (!this.selectedRelatedType()) { + return false; + } + var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), + prevRelatedIndex, + prevRelated; + + prevRelatedIndex = _.findLastIndex(relatedList, {id: this.preview().displayedRecord().id}) - 1; + prevRelated = relatedList[prevRelatedIndex]; + + if (typeof prevRelated === 'undefined') { + return true; + } + + return false; + }, + + /** + * Get next button disabled + * + * @param {Object} record + * + * @return {Boolean} + */ + getNextButtonDisabled: function (record) { + if (!this.selectedRelatedType()) { + return false; + } + var relatedList = this.selectedRelatedType() === 'series' ? record.series() : record.model(), + nextRelatedIndex, + nextRelated; + + nextRelatedIndex = _.findLastIndex(relatedList, {id: this.preview().displayedRecord().id}) + 1; + nextRelated = relatedList[nextRelatedIndex]; + + if (typeof nextRelated === 'undefined') { + return true; + } + + return false; + }, + + /** + * Switch image preview to related image + * + * @param {Object|null} relatedImage + * @param {Object} record + */ + switchImagePreviewToRelatedImage: function (relatedImage, record) { + if (!relatedImage) { + this.selectedRelatedType(null); + + return; + } + + if (this.preview().displayedRecord().id === relatedImage.id) { + return; + } + + this.preview().showRelated(relatedImage); + }, + + /** + * Switch image preview to series image + * + * @param {Object} series + * @param {Object} record + */ + switchImagePreviewToSeriesImage: function (series, record) { + this.selectedRelatedType('series'); + this.switchImagePreviewToRelatedImage(series, record); + }, + + /** + * Switch image preview to model image + * + * @param {Object} model + * @param {Object} record + */ + switchImagePreviewToModelImage: function (model, record) { + this.selectedRelatedType('model'); + this.switchImagePreviewToRelatedImage(model, record); + } + }); +}); diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/js/config.js b/AdobeStockImageAdminUi/view/adminhtml/web/js/config.js deleted file mode 100644 index 26b38682a163..000000000000 --- a/AdobeStockImageAdminUi/view/adminhtml/web/js/config.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -define([], function () { - 'use strict'; - - return { - mediaGallerySelector: '.media-gallery-modal:has(#search_adobe_stock)', - adobeStockModalSelector: '#adobe-stock-images-search-modal', - downloadPreviewUrl: 'adobe_stock/preview/download', - licenseAndDownloadUrl: 'adobe_stock/license/license', - confirmationUrl: 'adobe_stock/license/confirmation', - relatedImagesUrl: 'adobe_stock/preview/relatedimages' - }; -}); diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/panel.js b/AdobeStockImageAdminUi/view/adminhtml/web/panel.js index 418f569ffeed..45fb2385a630 100644 --- a/AdobeStockImageAdminUi/view/adminhtml/web/panel.js +++ b/AdobeStockImageAdminUi/view/adminhtml/web/panel.js @@ -6,9 +6,8 @@ define([ 'uiElement', 'jquery', - 'mage/translate', - 'Magento_AdobeStockImageAdminUi/js/config' -], function (Element, $, $t, config) { + 'mage/translate' +], function (Element, $, $t) { 'use strict'; return Element.extend({ @@ -28,12 +27,6 @@ define([ initialize: function () { this._super(); - config.downloadPreviewUrl = this.downloadPreviewUrl; - config.licenseAndDownloadUrl = this.licenseAndDownloadUrl; - config.buyCreditsUrl = this.buyCreditsUrl; - config.confirmationUrl = this.confirmationUrl; - config.relatedImagesUrl = this.relatedImagesUrl; - $(this.containerId).modal({ type: 'slide', buttons: [], diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/image-preview.html b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/image-preview.html index 4b0895a4b8da..132604bd49d9 100644 --- a/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/image-preview.html +++ b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/image-preview.html @@ -1,4 +1,3 @@ - -
-
-
- -
-
-
- - - - - + + + + +
@@ -59,76 +40,12 @@

- -

- -
-
- -
- - - -
- - -
+ + + + + + + diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/actions.html b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/actions.html new file mode 100644 index 000000000000..0ec1d49af894 --- /dev/null +++ b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/actions.html @@ -0,0 +1,29 @@ + +
    + +
    +
    +
    + +
    +
    +
    + +
+ + + diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/keywords.html b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/keywords.html new file mode 100644 index 000000000000..fd2a4b4e55d8 --- /dev/null +++ b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/keywords.html @@ -0,0 +1,19 @@ + +
+
+ +
+ + + +
+ + +
diff --git a/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/related.html b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/related.html new file mode 100644 index 000000000000..091e3b464b11 --- /dev/null +++ b/AdobeStockImageAdminUi/view/adminhtml/web/template/grid/column/preview/related.html @@ -0,0 +1,64 @@ + +
+ +
+ \ No newline at end of file diff --git a/AdobeUi/view/adminhtml/web/js/components/grid/column/image-preview.js b/AdobeUi/view/adminhtml/web/js/components/grid/column/image-preview.js index acd9a5c51afa..5938851bfab6 100644 --- a/AdobeUi/view/adminhtml/web/js/components/grid/column/image-preview.js +++ b/AdobeUi/view/adminhtml/web/js/components/grid/column/image-preview.js @@ -5,7 +5,7 @@ define([ 'underscore', 'jquery', - 'Magento_Ui/js/grid/columns/column', + 'Magento_Ui/js/grid/columns/column' ], function (_, $, Column) { 'use strict'; @@ -14,16 +14,48 @@ define([ previewImageSelector: '[data-image-preview]', visibility: [], height: 0, + previewStyles: {}, + displayedRecord: {}, lastOpenedImage: null, modules: { - masonry: '${ $.parentName }' + masonry: '${ $.parentName }', + thumbnailComponent: '${ $.parentName }.thumbnail_url', + }, + statefull: { + visible: true, + sorting: true, + lastOpenedImage: true + }, + listens: { + '${ $.provider }:params.filters': 'hide', + '${ $.provider }:params.search': 'hide' } }, + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'visibility', + 'height', + 'previewStyles', + 'displayedRecord', + 'lastOpenedImage' + ]); + this.height.subscribe(function () { + this.thumbnailComponent().previewHeight(this.height()); + }, this); + + return this; + }, + /** * Next image preview * - * @param record + * @param {Object} record */ next: function (record) { var recordToShow = this.getRecord(record._rowIndex + 1); @@ -34,7 +66,7 @@ define([ /** * Previous image preview * - * @param record + * @param {Object} record */ prev: function (record) { var recordToShow = this.getRecord(record._rowIndex - 1); @@ -73,6 +105,7 @@ define([ img; this.hide(); + this.displayedRecord(record); if (record.rowNumber) { this._selectRow(record.rowNumber); @@ -88,7 +121,7 @@ define([ } else { img.load(this._updateHeight.bind(this)); } - this.lastOpenedImage = record; + this.lastOpenedImage(record._rowIndex); }, /** @@ -106,7 +139,7 @@ define([ hide: function () { var visibility = this.visibility(); - this.lastOpenedImage = null; + this.lastOpenedImage(null); visibility.fill(false); this.visibility(visibility); this.height(0); @@ -120,8 +153,7 @@ define([ * @return {*|boolean} */ isVisible: function (record) { - if (this.lastOpenedImage - && this.lastOpenedImage._rowIndex === record._rowIndex + if (this.lastOpenedImage() === record._rowIndex && ( this.visibility()[record._rowIndex] === undefined || this.visibility()[record._rowIndex] === false @@ -130,6 +162,30 @@ define([ this.show(record); } return this.visibility()[record._rowIndex] || false; + }, + + /** + * Get styles for preview + * + * @returns {Object} + */ + getStyles: function () { + this.previewStyles({ + 'margin-top': '-' + this.height() + }); + + return this.previewStyles(); + }, + + /** + * Scroll to preview window + */ + scrollToPreview: function () { + $(this.previewImageSelector).get(0).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); } }); }); diff --git a/AdobeUi/view/adminhtml/web/js/components/grid/column/image.js b/AdobeUi/view/adminhtml/web/js/components/grid/column/image.js index 8469de69265b..b6858cf669c7 100644 --- a/AdobeUi/view/adminhtml/web/js/components/grid/column/image.js +++ b/AdobeUi/view/adminhtml/web/js/components/grid/column/image.js @@ -65,6 +65,7 @@ define([ styles['margin-bottom'] = this.previewRowId() === record.rowNumber ? this.previewHeight : 0; record.styles(styles); + return record.styles; }, diff --git a/AdobeUi/view/adminhtml/web/js/components/grid/column/overlay.js b/AdobeUi/view/adminhtml/web/js/components/grid/column/overlay.js index d14e4c83f975..2beeed6968f0 100644 --- a/AdobeUi/view/adminhtml/web/js/components/grid/column/overlay.js +++ b/AdobeUi/view/adminhtml/web/js/components/grid/column/overlay.js @@ -23,6 +23,7 @@ define([ return styles; } + return {}; } }); diff --git a/AdobeUi/view/adminhtml/web/js/components/grid/masonry.js b/AdobeUi/view/adminhtml/web/js/components/grid/masonry.js index f0cfa4d6570c..4ed80cf963f2 100644 --- a/AdobeUi/view/adminhtml/web/js/components/grid/masonry.js +++ b/AdobeUi/view/adminhtml/web/js/components/grid/masonry.js @@ -6,10 +6,10 @@ define([ 'Magento_Ui/js/grid/listing', 'jquery', 'ko' -], function (Element, $, ko) { +], function (Listing, $, ko) { 'use strict'; - return Element.extend({ + return Listing.extend({ defaults: { template: 'Magento_AdobeUi/grid/masonry', imports: { @@ -156,7 +156,7 @@ define([ * Wait for container to initialize */ waitForContainer: function (callback) { - if (typeof this.container === "undefined") { + if (typeof this.container === 'undefined') { setTimeout(function () { this.waitForContainer(callback); }.bind(this), 500); @@ -225,6 +225,6 @@ define([ */ hasData: function () { return !!this.rows() && !!this.rows().length; - }, + } }); }); From 6d88e62d02ba0814ececfb791820de58d6bb635e Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko Date: Sun, 6 Oct 2019 22:54:54 +0100 Subject: [PATCH 2/2] magento/adobe-stock-integration#510: Fixed static tests --- .../Ui/Component/Listing/Columns/ImagePreview.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php b/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php index 01404486373b..9c4f31fddf35 100644 --- a/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php +++ b/AdobeStockImageAdminUi/Ui/Component/Listing/Columns/ImagePreview.php @@ -59,4 +59,4 @@ public function prepare() ) ); } -} \ No newline at end of file +}